diff --git a/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt b/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt index e7fddc08..0c3f28dc 100644 --- a/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt +++ b/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt @@ -17,7 +17,8 @@ fun AppNavHost( modifier: Modifier = Modifier, contentPadding: PaddingValues, navController: NavHostController, - viewModelFactory: ViewModelProvider.Factory + viewModelFactory: ViewModelProvider.Factory, + onExit: () -> Unit = { /* ON EXIT HANDLING */ } ) { NavHost( modifier = modifier.padding( @@ -38,7 +39,8 @@ fun AppNavHost( }, onBookmarkServiceRequested = { navController.navigate("bookmark/${it.noticeId}/${it.noticeTitle}/${it.noticeInfo}") - } + }, + onExit = onExit ) bookmarkServiceGraph( @@ -47,7 +49,8 @@ fun AppNavHost( contentPadding = contentPadding, onNoticeDetailRequested = { target -> navController.navigate("noticeDetail/${target.nttId}/${Uri.encode(target.contentUrl)}/${target.isFabVisible}") - } + }, + onExit = onExit ) } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt b/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt index d14956ea..29b2c898 100644 --- a/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt +++ b/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -67,6 +66,7 @@ import com.doyoonkim.common.theme.title import com.doyoonkim.common.ui.PermissionRationaleComposable import com.doyoonkim.common.R import com.doyoonkim.notification.local.NotificationAlarmScheduler +import kotlinx.coroutines.delay import javax.inject.Inject @OptIn(ExperimentalMaterial3Api::class) @@ -121,6 +121,7 @@ class MainActivity : ComponentActivity() { } LaunchedEffect(Unit) { + delay(200L) // Reduce workload on MainThread on its first initialization. // Permission check requestPermissionLauncher.launch( arrayOf( @@ -216,13 +217,7 @@ class MainActivity : ComponentActivity() { enabled = true, onClick = { if (!bottomBarState.second) { - navController.navigate(NavRoutes.Home.route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(NavRoutes.Home.route) } }, icon = { @@ -243,13 +238,7 @@ class MainActivity : ComponentActivity() { enabled = true, onClick = { if (!bottomBarState.third) { - navController.navigate(NavRoutes.Bookmark.route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(NavRoutes.Bookmark.route) } }, icon = { @@ -276,7 +265,8 @@ class MainActivity : ComponentActivity() { modifier = Modifier, contentPadding = contentPadding, navController = navController, - viewModelFactory = viewModelFactory + viewModelFactory = viewModelFactory, + onExit = { activity.finish() } ) LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt b/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt index 91ee85de..4a493659 100644 --- a/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt +++ b/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt @@ -11,6 +11,7 @@ import com.doyoonkim.knutice.di.AppComponent import com.doyoonkim.knutice.di.DaggerAppComponent import com.doyoonkim.notification.fcm.PushNotificationService import com.doyoonkim.notification.fcm.TokenHandler +import kotlinx.coroutines.coroutineScope import javax.inject.Inject class MainApplication() : Application(), AppInjectorProvider { diff --git a/core/data/src/main/java/com/doyoonkim/data/repository/RemoteRepositoryImpl.kt b/core/data/src/main/java/com/doyoonkim/data/repository/RemoteRepositoryImpl.kt index c7384868..f3a5bf2a 100644 --- a/core/data/src/main/java/com/doyoonkim/data/repository/RemoteRepositoryImpl.kt +++ b/core/data/src/main/java/com/doyoonkim/data/repository/RemoteRepositoryImpl.kt @@ -10,7 +10,9 @@ import com.doyoonkim.network.KnuticeRemoteSource import com.doyoonkim.network.model.DeviceTokenRequest import com.doyoonkim.network.model.TopicSubscriptionPreferencesRequest import com.doyoonkim.network.model.UserReportRequest +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import model.NetworkResult import javax.inject.Inject @@ -30,7 +32,7 @@ class RemoteRepositoryImpl @Inject constructor( emit(null) } ) - } + }.flowOn(Dispatchers.IO) override fun queryNoticesPerPage(category: NoticeCategory, lastNttId: Int?) = flow { remoteSource.getNoticesPerPage(category, lastNttId).fold( diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchAllBookmarks.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchAllBookmarks.kt index 730e6656..8b7b5c8c 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchAllBookmarks.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchAllBookmarks.kt @@ -4,6 +4,7 @@ import com.doyoonkim.domain.LocalRepository import com.doyoonkim.model.BookmarkVO import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -24,6 +25,8 @@ class FetchAllBookmarksImpl @Inject constructor( nullable?.let { vo-> emit(Pair(bookmarkVO, vo)) } }) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeById.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeById.kt index 39173253..0bc4082e 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeById.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeById.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -17,5 +18,7 @@ class FetchNoticeByIdImpl @Inject constructor( override operator fun invoke(nttId: Int) = remoteRepository.queryNoticeById(nttId).transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeByIdFromLocal.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeByIdFromLocal.kt index cb9a1998..ee915900 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeByIdFromLocal.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeByIdFromLocal.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.LocalRepository import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -17,6 +18,8 @@ class FetchNoticeByIdFromLocalImpl @Inject constructor( override operator fun invoke(nttId: Int) = localRepository.queryNoticeById(nttId).transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesByKeyword.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesByKeyword.kt index 8f9b36bb..ed05938f 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesByKeyword.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesByKeyword.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -17,5 +18,7 @@ class FetchNoticesByKeywordImpl @Inject constructor( override operator fun invoke(keyword: String) = remoteRepository.queryNoticesByKeyword(keyword).transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesPerPage.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesPerPage.kt index 511683c2..b81102ce 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesPerPage.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesPerPage.kt @@ -4,6 +4,7 @@ import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.NoticeCategory import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -21,5 +22,7 @@ class FetchNoticesPerPageImpl @Inject constructor( else queryNoticesPerPage(category, lastNttId) }.transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopThreeNotices.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopThreeNotices.kt index 696d6ab0..22231f2b 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopThreeNotices.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopThreeNotices.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.TopThreeNoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -17,6 +18,8 @@ class FetchTopThreeNoticesImpl @Inject constructor( override operator fun invoke(): Flow = remoteRepository.queryTopThreeNotices().transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopicSubscriptionStatus.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopicSubscriptionStatus.kt index 8f226ddd..9021ae7b 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopicSubscriptionStatus.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopicSubscriptionStatus.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.TopicSubscriptionPreferencesVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -17,6 +18,8 @@ class FetchTopicSubscriptionStatusImpl @Inject constructor( override operator fun invoke() = remoteRepository.queryTopicSubscriptionStatus().transform { nullable -> nullable?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/ModifyBookmark.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ModifyBookmark.kt index 37089257..0c45f607 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/ModifyBookmark.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ModifyBookmark.kt @@ -5,6 +5,7 @@ import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.BookmarkVO import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -26,6 +27,8 @@ class ModifyBookmarkImpl @Inject constructor( override fun query(nttId: Int): Flow = localRepository.queryBookmarkByNttId(nttId).transform { result -> result?.let { emit(it) } + }.catch { + /* Internal Error. Consume values, and never emit values. */ } // TODO: Need to be revised later. (Is NoticeVO really required) @@ -35,9 +38,14 @@ class ModifyBookmarkImpl @Inject constructor( remoteRepository.queryNoticeById(bookmark.targetNoticeNttId).transform { result -> if (result != null) emitAll(localRepository.createBookmark(bookmark, result)) else emit(false) + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } else { localRepository.updateBookmark(bookmark) + .catch { + /* Internal Error. Consume values, and never emit values. */ + } } } @@ -45,6 +53,8 @@ class ModifyBookmarkImpl @Inject constructor( localRepository.requestBookmarkDeletion(bookmark).transform { result -> if (result) emitAll(localRepository.requestNoticeDeletion(notice)) else emit(false) + }.catch { + /* Internal Error. Consume values, and never emit values. */ } } diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitNotificationPreferences.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitNotificationPreferences.kt index 1d891f3e..cc040342 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitNotificationPreferences.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitNotificationPreferences.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import javax.inject.Inject interface SubmitNotificationPreferences { @@ -15,4 +16,7 @@ class SubmitNotificationPreferencesImpl @Inject constructor( override operator fun invoke(body: TopicSubscriptionPreferencesBody) = remoteRepository.requestTopicSubscriptionPreferencesSubmission(body) + .catch { + /* Internal Error. Consume values, and never emit values. */ + } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitUserReport.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitUserReport.kt index 6099c0f8..48f955f2 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitUserReport.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitUserReport.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.requestBody.UserReportBody import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import javax.inject.Inject interface SubmitUserReport { @@ -15,5 +16,8 @@ class SubmitUserReportImpl @Inject constructor( override operator fun invoke(body: UserReportBody) = remoteRepository.requestUserReportSubmission(body) + .catch { + /* Internal Error. Consume values, and never emit values. */ + } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/usecases/ValidateDeviceToken.kt b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ValidateDeviceToken.kt index 3c9f694c..993be486 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/usecases/ValidateDeviceToken.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ValidateDeviceToken.kt @@ -3,6 +3,7 @@ package com.doyoonkim.domain.usecases import com.doyoonkim.domain.RemoteRepository import com.doyoonkim.model.requestBody.DeviceTokenBody import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import javax.inject.Inject @@ -16,4 +17,8 @@ class ValidateDeviceTokenImpl @Inject constructor( override operator fun invoke(requestBody: DeviceTokenBody) = remoteRepository.requestTokenValidation(requestBody) + .catch { + /* Internal Error. Consume values, and never emit values. */ + emit(false) + } } \ No newline at end of file diff --git a/core/notification/src/main/java/com/doyoonkim/notification/fcm/TokenHandler.kt b/core/notification/src/main/java/com/doyoonkim/notification/fcm/TokenHandler.kt index 7d3f119b..b9052946 100644 --- a/core/notification/src/main/java/com/doyoonkim/notification/fcm/TokenHandler.kt +++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/TokenHandler.kt @@ -7,7 +7,10 @@ import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import javax.inject.Inject @@ -17,26 +20,32 @@ class TokenHandler @Inject constructor( private val TAG = this.javaClass.name fun handleCurrentTokenRequest() { - FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> - if (!task.isSuccessful) { - Log.d(TAG, "Incomplete task: ${task.exception}") - return@OnCompleteListener - } + val completeMarker = Job() + CoroutineScope(Dispatchers.IO + completeMarker).launch { + FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> + if (!task.isSuccessful) { + Log.d(TAG, "Incomplete task: ${task.exception}") + return@OnCompleteListener + } - // Get new FCM registration token - val registrationToken = task.result - Log.d(TAG, "Received Token: $registrationToken") + // Get new FCM registration token + val registrationToken = task.result + Log.d(TAG, "Received Token: $registrationToken") - // POST request to upload current token to the web server. - CoroutineScope(Dispatchers.IO).launch { - Log.d(TAG, "Start validating Token") - validateDeviceToken( - DeviceTokenBody(fcmToken = registrationToken) - ).collectLatest { result -> - if (result) Log.d(TAG, "Validation Successful") - else Log.d(TAG, "Unable to validate") + // POST request to upload current token to the web server. + CoroutineScope(Dispatchers.IO).launch { + Log.d(TAG, "Start validating Token") + validateDeviceToken(DeviceTokenBody(fcmToken = registrationToken)) + .flowOn(Dispatchers.IO) + .collectLatest { result -> + if (result) Log.d(TAG, "Validation Successful") + else Log.d(TAG, "Unable to validate") + } + completeMarker.complete() } - } - }) + }) + delay(5000L) + if (!completeMarker.isCompleted) completeMarker.complete() + } } } \ No newline at end of file diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/bookmarkServiceGraph.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/bookmarkServiceGraph.kt index 8ddbfcbd..82b435a3 100644 --- a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/bookmarkServiceGraph.kt +++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/bookmarkServiceGraph.kt @@ -22,7 +22,8 @@ fun NavGraphBuilder.bookmarkServiceGraph( navController: NavController, viewModelFactory: ViewModelProvider.Factory, contentPadding: PaddingValues, - onNoticeDetailRequested: (NoticeDetail) -> Unit + onNoticeDetailRequested: (NoticeDetail) -> Unit, + onExit: () -> Unit = { } ) { composable(NavRoutes.Bookmark.route) { @@ -33,7 +34,9 @@ fun NavGraphBuilder.bookmarkServiceGraph( onBookmarkSelected = { navController.navigate("bookmark/${it.noticeId}/${it.noticeTitle}/${it.noticeInfo}") }, - onBackPressed = { navController.popBackStack() } + onBackPressed = { + navController.popBackStack().also { if (!it) onExit() } + } ) } diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt index e3884eef..529b8b87 100644 --- a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt +++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt @@ -35,6 +35,7 @@ import com.doyoonkim.common.theme.containerBackgroundSolid import com.doyoonkim.common.R import com.doyoonkim.common.theme.subTitle import com.doyoonkim.common.ui.NotificationPreviewCardMarked +import kotlinx.coroutines.delay @Composable fun BookmarkListScreen( diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt index fb17f337..f7651166 100644 --- a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt +++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt @@ -7,6 +7,7 @@ import com.doyoonkim.domain.usecases.FetchAllBookmarks import com.doyoonkim.model.BookmarkVO import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -23,8 +24,8 @@ class BookmarkListViewModel @Inject constructor( val uiState = _uiState.asStateFlow() fun getAllBookmarks() { - updateFetchingStatus(false) viewModelScope.launch { + updateFetchingStatus(false).also { delay(200L) } fetchAllBookmarks() .flowOn(Dispatchers.IO) .collectLatest { result -> diff --git a/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt b/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt index 5b5decb6..63dd317a 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt @@ -38,7 +38,8 @@ fun NavGraphBuilder.mainServiceNavGraph( viewModelFactory: ViewModelProvider.Factory, contentPadding: PaddingValues, onNoticeDetailRequested: (NoticeDetail) -> Unit, - onBookmarkServiceRequested: (BookmarkInfo) -> Unit + onBookmarkServiceRequested: (BookmarkInfo) -> Unit, + onExit: () -> Unit = { } ) { // ViewModels will be injected via ViewModelFactory composable(NavRoutes.Home.route) { @@ -46,7 +47,9 @@ fun NavGraphBuilder.mainServiceNavGraph( modifier = Modifier.padding(5.dp), viewModel = viewModel(factory = viewModelFactory), bottomPadding = contentPadding.calculateBottomPadding(), - onGoBackAction = { navController.popBackStack() }, + onGoBackAction = { + navController.popBackStack().also { if (!it) onExit() } + }, onMoreNoticeRequested = { dest -> navController.run { when(dest) { diff --git a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt index a821da9d..6462a867 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt @@ -65,6 +65,7 @@ fun NoticeDetailScreen( if (noticeInfo.second.isNotBlank()) { Box( modifier = modifier + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)) ) { AndroidView( modifier = Modifier.fillMaxSize(), diff --git a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt index 0dac7b72..cc423a90 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt @@ -9,9 +9,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn @@ -59,6 +64,7 @@ fun NoticeSearchScreen( modifier = modifier .fillMaxWidth() .wrapContentHeight() + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)) ) { Row( modifier = Modifier diff --git a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt index 20c10a69..e3d41858 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt @@ -6,8 +6,13 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn @@ -61,6 +66,7 @@ fun NoticesInCategoryScreen( Box( modifier = modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.containerBackground) + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)) .pullRefresh(pullRefreshState) ) { LazyColumn( diff --git a/feature/main/src/main/java/com/doyoonkim/main/preference/NotificationPreferencesScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/NotificationPreferencesScreen.kt index cd52c3a4..a4673104 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/preference/NotificationPreferencesScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/preference/NotificationPreferencesScreen.kt @@ -7,17 +7,24 @@ import android.content.Context.NOTIFICATION_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.graphics.Paint.Align +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme @@ -72,122 +79,135 @@ fun NotificationPreferencesScreen( ) } - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top + Box( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)) ) { - Text( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - text = stringResource(R.string.pref_notification_title), - color = MaterialTheme.colorScheme.title, - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - textAlign = TextAlign.Start - ) - - HorizontalDivider( - Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.subTitle - ) - Column( - modifier = Modifier.wrapContentHeight() - .padding(top = 15.dp, bottom = 15.dp), + modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { - Row( - verticalAlignment = Alignment.CenterVertically + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.pref_notification_title), + color = MaterialTheme.colorScheme.title, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + textAlign = TextAlign.Start + ) + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + Column( + modifier = Modifier.wrapContentHeight() + .padding(top = 15.dp, bottom = 15.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top ) { - Column( - modifier = Modifier.wrapContentHeight().weight(5f), - verticalArrangement = Arrangement.spacedBy(5.dp) + Row( + verticalAlignment = Alignment.CenterVertically ) { - Text( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - text = stringResource(R.string.enable_notification_title), - color = MaterialTheme.colorScheme.title, - fontWeight = FontWeight.Medium, - fontSize = 18.sp, - textAlign = TextAlign.Start - ) - - Text( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - text = stringResource(R.string.enable_service_notification_sub), - color = MaterialTheme.colorScheme.subTitle, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - textAlign = TextAlign.Start + Column( + modifier = Modifier.wrapContentHeight().weight(5f), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.enable_notification_title), + color = MaterialTheme.colorScheme.title, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.enable_service_notification_sub), + color = MaterialTheme.colorScheme.subTitle, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + textAlign = TextAlign.Start + ) + } + + Switch( + checked = uiStatus.isMainNotificationPermissionGranted, + colors = SwitchDefaults.colors().copy( + checkedTrackColor = MaterialTheme.colorScheme.buttonPurple, + checkedThumbColor = Color.White + ), + onCheckedChange = { + val settingIntent = Intent( + "android.settings.APP_NOTIFICATION_SETTINGS" + ).apply { + this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + this.putExtra( + "android.provider.extra.APP_PACKAGE", + context.packageName + ) + } + context.startActivity(settingIntent) + }, + enabled = true ) } + } - Switch( - checked = uiStatus.isMainNotificationPermissionGranted, - colors = SwitchDefaults.colors().copy( - checkedTrackColor = MaterialTheme.colorScheme.buttonPurple, - checkedThumbColor = Color.White - ), - onCheckedChange = { - val settingIntent = Intent( - "android.settings.APP_NOTIFICATION_SETTINGS" - ).apply { - this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - this.putExtra( - "android.provider.extra.APP_PACKAGE", - context.packageName - ) - } - context.startActivity(settingIntent) - }, - enabled = true - ) + LabeledToggleSwitch( + modifier = Modifier.padding(start = 10.dp), + titleText = stringResource(R.string.general_notificaiton_channel_name), + subTitleText = stringResource(R.string.general_notification_channel_description), + isChecked = uiStatus.isEachChannelAllowed[0], + isEnabled = uiStatus.isMainNotificationPermissionGranted && uiStatus.isSyncCompleted + ) { + viewModel.updateChannelPreferenceState(0, it) } - } - LabeledToggleSwitch( - modifier = Modifier.padding(start = 10.dp), - titleText = stringResource(R.string.general_notificaiton_channel_name), - subTitleText = stringResource(R.string.general_notification_channel_description), - isChecked = uiStatus.isEachChannelAllowed[0], - isEnabled = uiStatus.isMainNotificationPermissionGranted - ) { - viewModel.updateChannelPreferenceState(0, it) - } + LabeledToggleSwitch( + modifier = Modifier.padding(start = 10.dp), + titleText = stringResource(R.string.academic_notification_channel_name), + subTitleText = stringResource(R.string.academic_notification_channel_description), + isChecked = uiStatus.isEachChannelAllowed[1], + isEnabled = uiStatus.isMainNotificationPermissionGranted && uiStatus.isSyncCompleted + ) { + viewModel.updateChannelPreferenceState(1, it) + } - LabeledToggleSwitch( - modifier = Modifier.padding(start = 10.dp), - titleText = stringResource(R.string.academic_notification_channel_name), - subTitleText = stringResource(R.string.academic_notification_channel_description), - isChecked = uiStatus.isEachChannelAllowed[1], - isEnabled = uiStatus.isMainNotificationPermissionGranted - ) { - viewModel.updateChannelPreferenceState(1, it) - } + LabeledToggleSwitch( + modifier = Modifier.padding(start = 10.dp), + titleText = stringResource(R.string.scholarship_notification_channel_name), + subTitleText = stringResource(R.string.scholarship_notification_channel_description), + isChecked = uiStatus.isEachChannelAllowed[2], + isEnabled = uiStatus.isMainNotificationPermissionGranted && uiStatus.isSyncCompleted + ) { + viewModel.updateChannelPreferenceState(2, it) + } - LabeledToggleSwitch( - modifier = Modifier.padding(start = 10.dp), - titleText = stringResource(R.string.scholarship_notification_channel_name), - subTitleText = stringResource(R.string.scholarship_notification_channel_description), - isChecked = uiStatus.isEachChannelAllowed[2], - isEnabled = uiStatus.isMainNotificationPermissionGranted - ) { - viewModel.updateChannelPreferenceState(2, it) + LabeledToggleSwitch( + modifier = Modifier.padding(start = 10.dp), + titleText = stringResource(R.string.event_notification_channel_name), + subTitleText = stringResource(R.string.event_notification_channel_description), + isChecked = uiStatus.isEachChannelAllowed[3], + isEnabled = uiStatus.isMainNotificationPermissionGranted && uiStatus.isSyncCompleted + ) { + viewModel.updateChannelPreferenceState(3, it) + } } - LabeledToggleSwitch( - modifier = Modifier.padding(start = 10.dp), - titleText = stringResource(R.string.event_notification_channel_name), - subTitleText = stringResource(R.string.event_notification_channel_description), - isChecked = uiStatus.isEachChannelAllowed[3], - isEnabled = uiStatus.isMainNotificationPermissionGranted - ) { - viewModel.updateChannelPreferenceState(3, it) + if (!uiStatus.isSyncCompleted) { + Box( + modifier = Modifier.matchParentSize() + .background(Color.Gray.copy(alpha = 0.5f)) + ) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } } - } - } fun isMainNotificationPermissionGranted( diff --git a/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt index 3f923c8d..ddcf9867 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt @@ -2,6 +2,11 @@ package com.doyoonkim.main.preference import android.webkit.WebView import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView @@ -14,7 +19,7 @@ fun OssNoticeScreen( BackHandler { onBackPressed() } AndroidView( - modifier = modifier, + modifier = modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)), factory = { context -> WebView(context).apply { loadUrl("https://knutice.github.io/KNUTICE-OpenSourceLicense/Android/opensource.html") diff --git a/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt index 68ace30c..b74e8cce 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt @@ -5,8 +5,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.HorizontalDivider @@ -40,7 +45,8 @@ fun UserPreferenceScreen( BackHandler { onBackPressed() } Column( - modifier = modifier, + modifier = modifier + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/HomeViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/HomeViewModel.kt index 689dae5f..21cdf08e 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/HomeViewModel.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/HomeViewModel.kt @@ -1,5 +1,6 @@ package com.doyoonkim.main.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.doyoonkim.domain.usecases.FetchTopThreeNoticesImpl @@ -7,6 +8,7 @@ import com.doyoonkim.model.NoticeVO import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt index 8e8e9501..0eea812b 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt @@ -1,5 +1,6 @@ package com.doyoonkim.main.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.doyoonkim.domain.usecases.FetchTopicSubscriptionStatusImpl @@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import javax.inject.Inject class NotificationPreferencesViewModel @Inject constructor( @@ -43,7 +45,7 @@ class NotificationPreferencesViewModel @Inject constructor( fun updateChannelPreferenceState(index: Int, state: Boolean) { _uiState.update { it.copy( - isEachChannelAllowed = it.isEachChannelAllowed.updateValueByIndex(index, state) + isSyncCompleted = false ) } @@ -59,10 +61,18 @@ class NotificationPreferencesViewModel @Inject constructor( if (!result) { _uiState.update { it.copy( - isEachChannelAllowed = it.isEachChannelAllowed.updateValueByIndex(index, !state), + isSyncCompleted = true, isError = true ) } + } else { + _uiState.update { + it.copy( + isEachChannelAllowed = it.isEachChannelAllowed.updateValueByIndex(index, state), + isSyncCompleted = true, + isError = false + ) + } } } } @@ -73,20 +83,33 @@ class NotificationPreferencesViewModel @Inject constructor( fun getTopicSubscriptionStatus() = viewModelScope.launch { - fetchTopicSubscriptionStatus() - .flowOn(Dispatchers.IO) - .collectLatest { status -> - _uiState.update { - it.copy( - isEachChannelAllowed = listOf( - status.general, - status.academic, - status.scholarship, - status.event + withTimeout(5000L) { + fetchTopicSubscriptionStatus() + .flowOn(Dispatchers.IO) + .collectLatest { status -> + _uiState.update { + it.copy( + isEachChannelAllowed = listOf( + status.general, + status.academic, + status.scholarship, + status.event + ), + isSyncCompleted = true, + isError = false ) - ) + } } + }.runCatching { + /* DO NOTHING (SYNC COMPLETED ON TIME. */ + }.onFailure { + _uiState.update { + it.copy( + isSyncCompleted = true, + isError = true + ) } + } } private fun List.updateValueByIndex(index: Int, value: Boolean) =