diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 74eedd49..075c2c1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,8 @@ android { applicationId = "com.doyoonkim.knutice" minSdk = 31 targetSdk = 34 - versionCode = 13 - versionName = "1.3.3" + versionCode = 15 + versionName = "1.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 298ce84e..61483746 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/Theme.KNUTICE" + android:windowSoftInputMode="adjustResize" tools:targetApi="31"> { + return runCatching { + knuticeService.create(KnuticeService::class.java).run { + this.getSingleNoticeById(id) + } + }.onFailure { throw it } + } + suspend fun queryNoticesByKeyword(keyword: String): NoticesPerPage { Log.d("KnuticeRemoteSource", "Start retrofit service (Querying Notices...)") return knuticeService.create(KnuticeService::class.java).queryNoticeByKeyword(keyword) } + suspend fun getTopicSubscriptionStatus(): Result { + return runCatching { + knuticeService.create(KnuticeService::class.java).getTopicSubscriptionStatus(validatedToken) + }.onFailure { throw it } + } + suspend fun getFullNoticeContent(url: String): Deferred = CoroutineScope(Dispatchers.IO).async { Jsoup.connect(url) @@ -135,44 +152,53 @@ class KnuticeRemoteSource @Inject constructor() { interface KnuticeService { - @GET("/open-api/notice/list") + @GET("open-api/notice/list") suspend fun getTopThreeNotice( @Query("noticeName") category: NoticeCategory, @Query("size") size: Int ): NoticesPerPage - @GET("/open-api/notice/list") + @GET("open-api/notice/list") suspend fun getNoticeListPerPage( @Query("noticeName") category: NoticeCategory, @Query("nttId") lastNttId: Int ): NoticesPerPage - @GET("/open-api/notice/list") + @GET("open-api/notice/list") suspend fun getFirstPageOfNotice( @Query("noticeName") category: NoticeCategory ): NoticesPerPage - @GET("/open-api/search") + @GET("open-api/notice/{nttId}") + suspend fun getSingleNoticeById( + @Path("nttId") nttId: String + ): SingleNotice + + @GET("open-api/search") suspend fun queryNoticeByKeyword( @Query("keyword") keyword: String ): NoticesPerPage @Headers("Content-Type: application/json") - @POST("/open-api/fcm") + @POST("open-api/fcm") suspend fun validateToken( @Body requestBody: ApiDeviceTokenRequest ): ApiPostResult @Headers("Content-Type: application/json") - @POST("/open-api/report") + @POST("open-api/report") suspend fun submitUserReport( @Body requestBody: ApiReportRequest ): ApiPostResult @Headers("Content-Type: application/json") - @POST("/open-api/topic") + @POST("open-api/topic") suspend fun submitTopicSubscriptionPreference( @Body requestBody: ApiTopicSubscriptionRequest ): ApiPostResult + @GET("open-api/topic") + suspend fun getTopicSubscriptionStatus( + @Header("fcmToken") token: String + ): TopicSubscriptionStatusDTO } diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt new file mode 100644 index 00000000..7afc319f --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt @@ -0,0 +1,34 @@ +package com.doyoonkim.knutice.domain + +import android.util.Log +import com.doyoonkim.knutice.data.KnuticeRemoteSource +import com.doyoonkim.knutice.model.Notice +import javax.inject.Inject + + +interface fetchSingleNotice { + suspend fun getSingleNoticeById(nttId: String): Notice +} + +class FetchSingleNoticeImpl @Inject constructor( + private val remoteSource: KnuticeRemoteSource +) : fetchSingleNotice { + + override suspend fun getSingleNoticeById(nttId: String): Notice { + remoteSource.getNoticeById(nttId).fold( + onSuccess = { + Log.d("FetchSingleNoticeImpl", it.notice?.toNotice().toString()) + return it.notice?.toNotice() ?: Notice() + }, + onFailure = { + Log.d("FetchSingleNoticeImpl", "Unable to receive notice") + return Notice() + } + ) + } + + suspend operator fun invoke(nttId: String): Notice { + return getSingleNoticeById(nttId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/fcm/PushNotificationHandler.kt b/app/src/main/java/com/doyoonkim/knutice/fcm/PushNotificationHandler.kt index e87d4715..b57c564d 100644 --- a/app/src/main/java/com/doyoonkim/knutice/fcm/PushNotificationHandler.kt +++ b/app/src/main/java/com/doyoonkim/knutice/fcm/PushNotificationHandler.kt @@ -2,14 +2,18 @@ package com.doyoonkim.knutice.fcm import android.Manifest import android.app.Notification +import android.app.PendingIntent +import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.Icon +import android.os.Bundle import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.doyoonkim.knutice.data.KnuticeRemoteSource import com.doyoonkim.knutice.R +import com.doyoonkim.knutice.presentation.MainActivity import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingService @@ -34,15 +38,19 @@ class PushNotificationHandler @Inject constructor() : FirebaseMessagingService() override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + // When the app is in background or killed, Data Payload would be delivered once the user + // clicks the system tray. Log.d(TAG, "Message data payload: ${message.notification}") if (message.data.isNotEmpty()) { - Log.d(TAG, "Message Data Payload: ${message.data}") - } + Log.d(TAG, "Message Data Payload: ${message.data}") // message.data: Map - message.notification?.let { - Log.d(TAG, "Body: ${it.body}") - message.toPushNotification() + // Apply "Do not disturb" option. (Temporarily save the message and deliver after the core time is end. + // Use Local Database (Room?) + message.notification?.let { + Log.d(TAG, "Body: ${it.body}") + message.toPushNotification() + } } } @@ -51,13 +59,15 @@ class PushNotificationHandler @Inject constructor() : FirebaseMessagingService() // Utilize channel already created by FCM as default val notificationBuilder = NotificationCompat.Builder( applicationContext, getString(R.string.inapp_notification_channel_id) - ) - .setSmallIcon(R.mipmap.ic_launcher) - .setLargeIcon(Icon.createWithResource(applicationContext, R.mipmap.ic_launcher)) - .setContentTitle(getString(R.string.new_notice)) - .setContentText(this@toPushNotification.notification?.body ?: "No message body") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) + ).apply { + setSmallIcon(R.mipmap.ic_launcher) + setLargeIcon(Icon.createWithResource(applicationContext, R.mipmap.ic_launcher)) + setContentTitle(getString(R.string.new_notice)) + setContentText(this@toPushNotification.notification?.body ?: "No message body.") + setPriority(NotificationCompat.PRIORITY_DEFAULT) + setAutoCancel(true) + } + with(NotificationManagerCompat.from(applicationContext)) { if (ActivityCompat.checkSelfPermission( diff --git a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt b/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt index 475839ab..9a6a9575 100644 --- a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt +++ b/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt @@ -33,6 +33,46 @@ data class TopThreeNotices( ) } +data class TopicSubscriptionStatusDTO( + @SerializedName("result") var result: Result? = Result(), + @SerializedName("body") var body: Body? = Body() +) { + data class Body( + @SerializedName("generalNewsTopic") var generalNewsTopic: Boolean = false, + @SerializedName("scholarshipNewsTopic") var scholarshipNewsTopic: Boolean = false, + @SerializedName("eventNewsTopic") var eventNewsTopic: Boolean = false, + @SerializedName("academicNewsTopic") var academicNewsTopic: Boolean = false, + @SerializedName("employmentNewsTopic") var employmentNewsTopic: Boolean = false + ) +} + +data class SingleNotice( + @SerializedName("result") var result: Result? = Result(), + @SerializedName("body") var notice: NoticeDTO? +) { + data class NoticeDTO( + @SerializedName("nttId") var nttId: Int, + @SerializedName("title") var title: String, + @SerializedName("contentUrl") var contentUrl: String, + @SerializedName("contentImage") var contentImage: String?, + @SerializedName("departmentName") var departmentName: String, + @SerializedName("registeredAt") var registeredAt: String, + @SerializedName("noticeName") var noticeName: String + ) { + fun toNotice(): Notice { + return Notice( + nttId = nttId, + title = title, + url = contentUrl, + imageUrl = contentImage ?: "", + departName = departmentName, + timestamp = registeredAt, + // noticeCategory = noticeName + ) + } + } +} + data class NoticesPerPage( @SerializedName("result") var result: Result? = Result(), @SerializedName("body") var body: ArrayList = arrayListOf() @@ -92,7 +132,8 @@ data class Notice( title, "[$departName] $timestamp", url, - imageUrl + imageUrl, + nttId.toString() ) } @@ -111,6 +152,7 @@ data class Notice( // DetailedNoticeContent data class DetailedContentState( + val requestedNotice: Notice = Notice(), val url: String = "", val title: String = "", val info: String = "", @@ -140,7 +182,9 @@ data class SearchNoticeState( data class NotificationPreferenceStatus( val isMainNotificationPermissionGranted: Boolean = false, //TODO: Consider change data type to MAP - val isEachChannelAllowed: List = listOf(true, true, true, true) + val isEachChannelAllowed: List = listOf(false, false, false, false), + val isSyncCompleted: Boolean = false, + val isError: Boolean = false ) // BookmarkComposable diff --git a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt b/app/src/main/java/com/doyoonkim/knutice/model/Types.kt index 08dee5c5..bb2f99e7 100644 --- a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt +++ b/app/src/main/java/com/doyoonkim/knutice/model/Types.kt @@ -13,10 +13,13 @@ data class NavDestination( val arrived: Destination = Destination.Unspecified ) + +// TODO: Change structure of FullContent (FullContent(notice: Notice, url: String)) @Serializable data class FullContent( val title: String? = null, val info: String? = null, val url: String, - val imgUrl: String + val imgUrl: String, + val nttId: String, ) \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt b/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt index d3d026a3..c83eb903 100644 --- a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt +++ b/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt @@ -33,7 +33,7 @@ import com.doyoonkim.knutice.viewModel.MainServiceViewModel fun MainNavigator( modifier: Modifier = Modifier, viewModel: MainServiceViewModel = hiltViewModel(), - navController: NavHostController, + navController: NavHostController ) { // Navigation NavHost( @@ -106,11 +106,21 @@ fun MainNavigator( composable { backStackEntry -> val requestedNotice = backStackEntry.toRoute() val scaffoldTitle = requestedNotice.title - viewModel.updateState( - updatedCurrentLocation = Destination.DETAILED, - updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content", - updatedBottomNavBarVisibility = true - ) + + viewModel.run { + if (uiState.value.currentLocation != Destination.DETAILED) { + updateState( + updatedCurrentLocation = Destination.DETAILED, + updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content", + updatedBottomNavBarVisibility = true + ) + + // Need to find the reason for multiple request caused by multiple recomposition + if (requestedNotice.nttId != uiState.value.tempReserveNoticeForBookmark.nttId.toString()) { + getReservedNotice(requestedNotice.nttId) + } + } + } DetailedNoticeContent() Spacer(Modifier.height(20.dp)) } @@ -120,7 +130,15 @@ fun MainNavigator( updatedCurrentLocation = Destination.EDIT_BOOKMARK, updatedBottomNavBarVisibility = false ) - EditBookmark(Modifier.padding(10.dp)) { bookmark -> + EditBookmark( + Modifier.padding(10.dp), + onDetailedNoticeRequested = { + viewModel.updateState( + updatedFabVisibility = false, + ) + navController.navigate(it.toFullContent()) + } + ) { bookmark -> Log.d("MainNavigator", "Bookmark instance received: ${bookmark ?: "null"}") // onSavedClicked if (bookmark != null) { diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt index 1d348a2c..15760329 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt @@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.doyoonkim.knutice.R import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.component.NotificationPreviewCardMarked +import com.doyoonkim.knutice.ui.theme.containerBackgroundSolid import com.doyoonkim.knutice.ui.theme.displayBackground import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.viewModel.BookmarkViewModel @@ -50,7 +51,7 @@ fun BookmarkComposable( // } Box( - modifier = modifier.background(MaterialTheme.colorScheme.displayBackground) + modifier = modifier.background(MaterialTheme.colorScheme.containerBackgroundSolid) .systemBarsPadding() ) { if (uiState.bookmarks.isEmpty()) { diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt index 782b8728..cd628425 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt @@ -7,8 +7,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding @@ -49,6 +52,8 @@ fun CustomerService( onCloseRequested: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsState() + val adjustImePadding = Modifier.consumeWindowInsets(WindowInsets.ime).imePadding() + Box( modifier = modifier.fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars) @@ -76,6 +81,7 @@ fun CustomerService( Box( modifier = Modifier.fillMaxWidth().weight(5f) .padding(top = 25.dp, bottom = 25.dp) + .then(adjustImePadding) ) { TextField( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt index 278cc3ba..b5485eb6 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt @@ -1,12 +1,20 @@ package com.doyoonkim.knutice.presentation +import android.app.DownloadManager +import android.content.Context.DOWNLOAD_SERVICE +import android.content.Intent import android.content.res.Configuration +import android.net.Uri +import android.os.Environment import android.util.Log import android.view.View +import android.webkit.CookieManager +import android.webkit.URLUtil import android.webkit.WebChromeClient import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,7 +23,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,8 +33,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.doyoonkim.knutice.R +import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.ui.theme.displayBackground import com.doyoonkim.knutice.viewModel.DetailedNoticeContentViewModel +import okio.Path.Companion.toPath @Composable fun DetailedNoticeContent( @@ -44,41 +57,23 @@ fun DetailedNoticeContent( state.loadingStatue } ) - AndroidView( - modifier = Modifier, - factory = { context -> - WebView(context).apply { - //Enable Javascript - // Security Alert: XSS Vulnerability - settings.javaScriptEnabled = true - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - val theme = context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) - when (theme) { - Configuration.UI_MODE_NIGHT_YES -> { - evaluateJavascript( - """ - var themeStyle = 'div, p, span, ul { background-color: #262729 !important; color: #ffffff !important; } .bbs_detail { border-top: 0px; } .bbs_detail_tit, .info { background-color: #333437 !important; color: #ffffff !important; border-radius: 15px; border-bottom: 0px; } .bbs_detail_tit h2 {color: #ffffff !important } .bbs_detail_tit .info li { color: #ffffff !important } .bbs_detail span { color: #ffffff !important } .bbs_detail_file { background-color: #787879 !important; color: #ffffff !important; border-radius: 15px; margin-top: 10px; padding: 15px; } .bbs_detail_file a { color: #ffffff; }', - head = document.head || document.getElementsByTagName('head')[0], - style = document.createElement('style'); - - head.appendChild(style); - style.type = 'text/css'; - if (style.styleSheet) { - style.styleSheet.cssText = themeStyle; - } else { - style.appendChild(document.createTextNode(themeStyle)); - } - """.trimIndent() - ) { - Log.d("DetailedNoticeContent", "Dark Theme Applied") - } - } - } + if (state.url.isNotBlank() || state.requestedNotice.url != "Unknown") { + AndroidView( + modifier = Modifier, + factory = { context -> + WebView(context).apply { + //Enable Javascript + // Security Alert: XSS Vulnerability + settings.javaScriptEnabled = true + settings.defaultTextEncodingName = "UTF-8" - evaluateJavascript( - """ + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + val theme = context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) + + evaluateJavascript( + """ let div_accessibility = document.getElementById('accessibility'); let div_header = document.getElementById('header'); let div_point = document.getElementById('point'); @@ -93,39 +88,64 @@ fun DetailedNoticeContent( div_accessibility.remove(); div_header.remove(); - div_point.remove(); div_footer.remove(); - div_footer_root.remove(); - section_svisual.remove(); - section_location.remove(); aside_remote.remove(); p_board_butt[0].remove(); """.trimIndent(), - ) { result -> - Log.d("Android Web View Client", "RESULT: $result") - visibility = View.VISIBLE + ) { result -> + Log.d("Android Web View Client", "RESULT: $result") + visibility = View.VISIBLE + } + super.onPageFinished(view, url) } - super.onPageFinished(view, url) } - } - // For Progress Indicator - webChromeClient = object: WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - // Update progress status - viewModel.updateLoadingStatus(newProgress) - super.onProgressChanged(view, newProgress) + // For Progress Indicator + webChromeClient = object: WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + // Update progress status + viewModel.updateLoadingStatus(newProgress) + super.onProgressChanged(view, newProgress) + } } - } - visibility = View.INVISIBLE - settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW - loadUrl(state.url) + setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + val request = DownloadManager.Request(Uri.parse(url)) + val filename = URLUtil.guessFileName(url, contentDisposition, mimetype).also { Log.d("DownloadManager", "Filename: $it") } + // save session data before downloading the target file. + val cookies = CookieManager.getInstance().getCookie(url) + + request.apply { + setMimeType(mimetype) + addRequestHeader("cookie", cookies) + addRequestHeader("User-Agent", userAgent) + setDescription("Downloading File") + setTitle(filename) +// allowScanningByMediaScanner() Deprecated. + setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) + } + val downloadManager = context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager + downloadManager.enqueue(request).also { + Toast.makeText(context, R.string.text_download, Toast.LENGTH_LONG).show() + // Guide user to the File application + context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)) + } + } + + visibility = View.INVISIBLE + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + if (state.url != "Unknown") { + loadUrl(state.url) + } else { + loadUrl(state.requestedNotice.url) + } + } } - } - ) + ) + } } } diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt index 0c744e07..71807e57 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt @@ -12,13 +12,27 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset +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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -47,6 +61,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.doyoonkim.knutice.R import com.doyoonkim.knutice.model.Bookmark +import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.component.DateTimePicker import com.doyoonkim.knutice.presentation.component.NotificationPreviewCard import com.doyoonkim.knutice.ui.theme.containerBackground @@ -65,10 +80,11 @@ import java.util.TimeZone fun EditBookmark( modifier: Modifier = Modifier, viewModel: EditBookmarkViewModel = hiltViewModel(), + onDetailedNoticeRequested: (Notice) -> Unit, onSaveClicked: (Bookmark?) -> Unit = { } ) { val uiState by viewModel.uiState.collectAsState() - val localContext = LocalContext.current + val adjustImePadding = Modifier.consumeWindowInsets(WindowInsets.ime).imePadding() LaunchedEffect(uiState.isReminderRequested) { if (uiState.timeForRemind == 0L) { @@ -83,7 +99,10 @@ fun EditBookmark( modifier = Modifier.padding(5.dp), notificationTitle = uiState.targetNotice.title, notificationInfo = uiState.targetNotice.departName - ) + ) { + // Request Full Content + onDetailedNoticeRequested(uiState.targetNotice) + } Spacer(Modifier.height(30.dp)) @@ -166,11 +185,14 @@ fun EditBookmark( ) Box( - modifier = Modifier.fillMaxWidth().weight(5f) - .padding(top = 5.dp, bottom = 25.dp) + modifier = Modifier.fillMaxWidth() + .fillMaxHeight() + .weight(1f) + .padding(top = 5.dp, bottom = 20.dp) + .then(adjustImePadding) ) { TextField( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().padding(bottom = 5.dp), value = uiState.bookmarkNote, placeholder = { Text(text = stringResource(R.string.placeholder_notes)) }, enabled = true, @@ -207,7 +229,8 @@ fun EditBookmark( ) { val coroutineScope = rememberCoroutineScope() Button( - modifier = Modifier.wrapContentHeight().weight(1f), + modifier = Modifier.wrapContentHeight() + .weight(1f), enabled = true, shape = RoundedCornerShape(10.dp), onClick = { @@ -253,7 +276,7 @@ fun EditBookmark( // DateTimePicker AnimatedVisibility( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), + modifier = Modifier.fillMaxWidth().wrapContentHeight().imePadding(), visible = uiState.datePickerVisible, enter = slideInVertically(initialOffsetY = { it + it / 2 }), exit = slideOutVertically(targetOffsetY = { it / 2 }) @@ -291,6 +314,6 @@ private fun Long.toFormattedDate(f: SimpleDateFormat): String { @Preview(showBackground = true) @Composable fun EditBookmark_Preview() { - EditBookmark(Modifier.fillMaxSize().padding(10.dp)) { } +// EditBookmark(Modifier.fillMaxSize().padding(10.dp)) { } } diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt index 8b10b250..835ffedb 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt @@ -13,10 +13,16 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,10 +33,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import androidx.navigation.compose.rememberNavController import com.doyoonkim.knutice.R import com.doyoonkim.knutice.alarm.NotificationAlarmScheduler +import com.doyoonkim.knutice.model.FullContent +import com.doyoonkim.knutice.navigation.MainNavigator import com.doyoonkim.knutice.presentation.component.PermissionRationaleComposable import com.doyoonkim.knutice.ui.theme.KNUTICETheme +import com.doyoonkim.knutice.ui.theme.containerBackgroundSolid import dagger.hilt.android.AndroidEntryPoint import java.util.Locale @@ -44,10 +55,12 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // applicationContext.deleteDatabase("Main Local Database") + WindowCompat.setDecorFitsSystemWindows(window, false) enableEdgeToEdge() setContent { KNUTICETheme { val context = LocalContext.current + val navController = rememberNavController() var showPermissionRationale by remember { mutableStateOf(false) } // Permission Launcher @@ -71,13 +84,47 @@ class MainActivity : ComponentActivity() { Manifest.permission.SCHEDULE_EXACT_ALARM ) ) - } - MainServiceScreen() { alarmTarget -> - if (!alarmTarget.isScheduled) notificationAlarmScheduler.cancel(alarmTarget) - else notificationAlarmScheduler.schedule(alarmTarget) + // Handling Push Notification Click Action + // Check there's a extra or not. + val requestedIntent = this@MainActivity.intent + if (requestedIntent.getStringExtra("nttId") != null) { + // Navigate to Detailed Notice + + // When the app is in background or killed, Data Payload would be delivered once the user + // clicks the system tray. (Data Payload will be delivered as Intent) + FullContent( + requestedIntent.getStringExtra("contentTitle"), + requestedIntent.getStringExtra("noticeName"), + requestedIntent.getStringExtra("contentUrl") ?: "", + requestedIntent.getStringExtra("contentImage") ?: "", + requestedIntent.getStringExtra("nttId").toString() + ).run { + navController.navigate(this) + } + } } + MainServiceScreen( + navController = navController, + onScheduleAlarmTriggered = { + alarmTarget -> + if (!alarmTarget.isScheduled) notificationAlarmScheduler.cancel(alarmTarget) + else notificationAlarmScheduler.schedule(alarmTarget) + } + ) { innerPadding -> + MainNavigator( + navController = navController, modifier = Modifier + .consumeWindowInsets(WindowInsets.systemBars) + .padding( + PaddingValues( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding() + ) + ) + .background(MaterialTheme.colorScheme.containerBackgroundSolid) + ) + } AnimatedVisibility( visible = showPermissionRationale, @@ -112,6 +159,12 @@ class MainActivity : ComponentActivity() { } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + + } + override fun onDestroy() { super.onDestroy() diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt index 42ce2862..75444dce 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.wrapContentSize @@ -23,6 +24,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -44,15 +46,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.doyoonkim.knutice.R import com.doyoonkim.knutice.model.Bookmark import com.doyoonkim.knutice.model.Destination import com.doyoonkim.knutice.model.NavDestination import com.doyoonkim.knutice.navigation.MainNavigator +import com.doyoonkim.knutice.ui.theme.bottomNavContainer import com.doyoonkim.knutice.ui.theme.containerBackground +import com.doyoonkim.knutice.ui.theme.containerBackgroundSolid import com.doyoonkim.knutice.ui.theme.displayBackground import com.doyoonkim.knutice.ui.theme.subTitle +import com.doyoonkim.knutice.ui.theme.textPurple import com.doyoonkim.knutice.ui.theme.title import com.doyoonkim.knutice.viewModel.MainServiceViewModel import kotlinx.coroutines.async @@ -61,10 +68,11 @@ import kotlinx.coroutines.async @Composable fun MainServiceScreen( viewModel: MainServiceViewModel = hiltViewModel(), - onScheduleAlarmTriggered: (Bookmark) -> Unit, // Should be refactored later + navController: NavHostController, + onScheduleAlarmTriggered: (Bookmark) -> Unit, // Should be refactored later + content: @Composable (PaddingValues) -> Unit ) { val mainAppState by viewModel.uiState.collectAsState() - val navController = rememberNavController() // //TODO Live translation feature (TBD) // var showLanguageDownloadRationale by remember { mutableStateOf(false) } @@ -103,7 +111,7 @@ fun MainServiceScreen( Scaffold( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.displayBackground), + .background(MaterialTheme.colorScheme.containerBackgroundSolid), topBar = { TopAppBar( title = { @@ -117,7 +125,9 @@ fun MainServiceScreen( ) { IconButton( onClick = { - navController.popBackStack() + navController.popBackStack().also { + viewModel.updateState(updatedFabVisibility = true) + } } ) { Image( @@ -166,7 +176,7 @@ fun MainServiceScreen( }, colors = TopAppBarDefaults.topAppBarColors( titleContentColor = MaterialTheme.colorScheme.title, - containerColor = MaterialTheme.colorScheme.containerBackground + containerColor = MaterialTheme.colorScheme.containerBackgroundSolid ), actions = { if (mainAppState.currentLocation == Destination.MAIN) { @@ -199,21 +209,31 @@ fun MainServiceScreen( ) }, floatingActionButton = { - if (mainAppState.currentLocation == Destination.DETAILED) { + if (mainAppState.currentLocation == Destination.DETAILED && mainAppState.isFabVisible) { FloatingActionButton( onClick = { if (mainAppState.tempReserveNoticeForBookmark.title.isNotBlank()) { navController.navigate(mainAppState.tempReserveNoticeForBookmark) } + }, + containerColor = if (mainAppState.tempReserveNoticeForBookmark.title.isNotBlank()) { + MaterialTheme.colorScheme.textPurple + } else { + MaterialTheme.colorScheme.subTitle } ) { - Icon(Icons.Filled.Add, "Floating Action Button") + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Floating Action Button", + tint = Color.White + ) } } }, bottomBar = { AnimatedVisibility( - visible = mainAppState.isBottomNavBarVisible, + visible = mainAppState.currentLocation == Destination.MAIN + || mainAppState.currentLocation == Destination.BOOKMARKS, enter = slideInVertically(initialOffsetY = { it + (it * 1 / 2) }), exit = slideOutVertically(targetOffsetY = { it + (it * 1 / 2) }) ) { @@ -262,7 +282,7 @@ fun MainServiceScreen( unselectedContentColor = MaterialTheme.colorScheme.subTitle ) }, - containerColor = MaterialTheme.colorScheme.containerBackground, + containerColor = MaterialTheme.colorScheme.bottomNavContainer, contentColor = MaterialTheme.colorScheme.title ) } @@ -272,18 +292,8 @@ fun MainServiceScreen( }, containerColor = Color.Transparent ) { innerPadding -> - val adjustmentFactor = 10.dp - MainNavigator( - navController = navController, modifier = Modifier - .consumeWindowInsets(WindowInsets.systemBars) - .padding( - PaddingValues( - top = innerPadding.calculateTopPadding(), - bottom = innerPadding.calculateBottomPadding() - ) - ) - .background(MaterialTheme.colorScheme.displayBackground) - ) + + content(innerPadding) //TODO Live Translation Feature (TBD) // AnimatedVisibility( diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt index 768de7d2..50ac36f6 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt @@ -2,6 +2,7 @@ package com.doyoonkim.knutice.presentation import android.util.Log import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -30,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.component.NotificationPreview import com.doyoonkim.knutice.ui.theme.containerBackground +import com.doyoonkim.knutice.ui.theme.containerBackgroundSolid import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.viewModel.MoreCategorizedNotificationViewModel @@ -56,6 +58,7 @@ fun MoreCategorizedNotification( Box( modifier = modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.containerBackground) .pullRefresh(pullRefreshState) ) { LaunchedEffect(Unit) { @@ -85,10 +88,11 @@ fun MoreCategorizedNotification( viewModel.requestMoreNotices() } else { val notice = uiState.notices[index] - if (!uiState.isLoading) { + if (index != 0 || !uiState.isLoading) { Divider( Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp), - color = MaterialTheme.colorScheme.containerBackground + color = MaterialTheme.colorScheme.containerBackgroundSolid, + thickness = 1.3.dp ) } Row( diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt index 254c162a..921020cf 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt @@ -1,6 +1,7 @@ package com.doyoonkim.knutice.presentation import android.content.Intent +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -15,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -32,6 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LifecycleEventEffect import com.doyoonkim.knutice.R import com.doyoonkim.knutice.model.NoticeCategory @@ -52,6 +55,7 @@ fun NotificationPreferences( LaunchedEffect(Unit) { viewModel.checkMainNotificationPreferenceStatus() + viewModel.checkTopicSubscriptionStatus() } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { @@ -59,7 +63,9 @@ fun NotificationPreferences( } Column( - modifier = Modifier.fillMaxSize().systemBarsPadding(), + modifier = Modifier.fillMaxSize() + .systemBarsPadding() + .padding(10.dp), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt index 55575a35..86fa71df 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt @@ -64,7 +64,7 @@ fun NotificationPreview( .padding(top = 7.dp, start = 5.dp, end = 5.dp), text = notificationTitle, textAlign = TextAlign.Start, - fontSize = 13.sp, + fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.title, maxLines = 1, @@ -72,10 +72,10 @@ fun NotificationPreview( ) Text( modifier = Modifier.fillMaxWidth() - .padding(top = 1.dp, start = 5.dp, bottom = 5.dp, end = 5.dp), + .padding(start = 5.dp, bottom = 5.dp, end = 5.dp), text = notificationInfo, textAlign = TextAlign.Start, - fontSize = 9.sp, + fontSize = 12.sp, color = MaterialTheme.colorScheme.subTitle ) } diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt index 2f1867e7..3f67b694 100644 --- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt +++ b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt @@ -19,8 +19,13 @@ val Notification04 = Color(0xFF4294F7) val WhiteBackground = Color(0xFFFFFFFF) val DarkBackground = Color(0xFF262729) -val ContainerWhite = Color(0xFFF3F3F3) +val ContainerLight = Color(0xFFF3F3F3) val ContainerDark = Color(0xFF333437) +val ContainerBlack = Color(0xFF000000) +val ContainerWhite = Color(0xFFFFFFFF) + +val bottomNavBarWhite = Color(0xFFFFFFFF) +val bottomNavBarBlack = Color(0xFF424448) val TitleBlack = Color(0xFF000000) val TitleWhite = Color(0xFFFFFFFF) diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt index 7ee5bc76..01d391f9 100644 --- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt +++ b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt @@ -59,6 +59,14 @@ val ColorScheme.containerBackground: Color @Composable get() = if(isSystemInDarkTheme()) ContainerDark else ContainerWhite +val ColorScheme.containerBackgroundSolid: Color + @Composable + get() = if(isSystemInDarkTheme()) ContainerBlack else ContainerLight + +val ColorScheme.bottomNavContainer: Color + @Composable + get() = if(isSystemInDarkTheme()) bottomNavBarBlack else bottomNavBarWhite + val ColorScheme.title: Color @Composable get() = if(isSystemInDarkTheme()) TitleWhite else TitleBlack diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt index 446a65b7..0af7ea17 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt @@ -4,13 +4,18 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.toRoute +import com.doyoonkim.knutice.data.KnuticeRemoteSource import com.doyoonkim.knutice.domain.CrawlFullContentImpl +import com.doyoonkim.knutice.domain.FetchSingleNoticeImpl import com.doyoonkim.knutice.model.DetailedContentState import com.doyoonkim.knutice.model.FullContent +import com.doyoonkim.knutice.model.Notice import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -22,7 +27,7 @@ import javax.inject.Inject @HiltViewModel class DetailedNoticeContentViewModel @Inject constructor( - private val crawlFullContentUseCase: CrawlFullContentImpl, + private val fetchSingleNoticeImpl: FetchSingleNoticeImpl, private val savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -37,6 +42,7 @@ class DetailedNoticeContentViewModel @Inject constructor( url = requested.url ) } + requestNoticeById(requested.nttId!!) } fun updateLoadingStatus(newStatus: Int) { @@ -50,31 +56,15 @@ class DetailedNoticeContentViewModel @Inject constructor( } } - fun requestFullContent() { - CoroutineScope(Dispatchers.IO).launch { - crawlFullContentUseCase.getFullContentFromSource( - requested.title ?: "", requested.info ?: "", requested.url - ) - .map { Result.success(it) } - .catch { emit(Result.failure(it)) } - .collectLatest { result -> - result.fold( - onSuccess = { content -> - _uiState.update { - it.copy( - title = content.title, - info = content.info, - fullContent = content.fullContent, - fullContentUrl = content.fullContentUrl, - imageUrl = requested.imgUrl - ) - } - }, - onFailure = { - Log.d("DetailedNoticeContentVM", "Unable to get Full Content.") - } - ) - } + private fun requestNoticeById(nttId: String) { + viewModelScope.launch { + val requested = async { fetchSingleNoticeImpl.getSingleNoticeById(nttId) } + + _uiState.update { + it.copy( + requestedNotice = requested.await() + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/MainServiceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/MainServiceViewModel.kt index 4a7fca64..444cf3a7 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/MainServiceViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/MainServiceViewModel.kt @@ -1,24 +1,35 @@ package com.doyoonkim.knutice.viewModel +import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.doyoonkim.knutice.data.KnuticeRemoteSource +import com.doyoonkim.knutice.domain.FetchSingleNoticeImpl import com.doyoonkim.knutice.model.Bookmark import com.doyoonkim.knutice.model.Destination import com.doyoonkim.knutice.model.Notice import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class MainServiceViewModel @Inject constructor() : ViewModel() { +class MainServiceViewModel @Inject constructor( + private val fetchSingleNotice: FetchSingleNoticeImpl +) : ViewModel() { private var _uiState = MutableStateFlow(MainServiceState()) val uiState = _uiState.asStateFlow() + // Targeted to be separated. (Causes multiple recomposition & state updates.) fun updateState( updatedCurrentLocation: Destination = _uiState.value.currentLocation, updatedCurrentScaffoldTitle: String = _uiState.value.currentScaffoldTitle, updatedBottomNavBarVisibility: Boolean = _uiState.value.isBottomNavBarVisible, + updatedFabVisibility: Boolean = _uiState.value.isFabVisible, updatedTempReservedNoticeForBookmark: Notice = _uiState.value.tempReserveNoticeForBookmark, updatedCurrentTargetBookmark: Bookmark = _uiState.value.currentTargetBookmark, updatedScheduleTriggered: Boolean = _uiState.value.scheduleTriggered @@ -28,6 +39,7 @@ class MainServiceViewModel @Inject constructor() : ViewModel() { currentLocation = updatedCurrentLocation, currentScaffoldTitle = updatedCurrentScaffoldTitle, isBottomNavBarVisible = updatedBottomNavBarVisibility, + isFabVisible = updatedFabVisibility, tempReserveNoticeForBookmark = updatedTempReservedNoticeForBookmark, currentTargetBookmark = updatedCurrentTargetBookmark, scheduleTriggered = updatedScheduleTriggered @@ -42,13 +54,30 @@ class MainServiceViewModel @Inject constructor() : ViewModel() { ) } } + + fun getReservedNotice(nttId: String) { + viewModelScope.launch { + Log.d("MainServiceViewModel", "Start request reserved notice") + val request = async { + fetchSingleNotice.getSingleNoticeById(nttId) + }.await() + + _uiState.update { + it.copy( + tempReserveNoticeForBookmark = request, + currentScaffoldTitle = request.title + ) + } + } + } } data class MainServiceState( val currentLocation: Destination = Destination.MAIN, val currentScaffoldTitle: String = "", val isBottomNavBarVisible: Boolean = false, - val tempReserveNoticeForBookmark: Notice = Notice(), // ? + val isFabVisible: Boolean = true, + val tempReserveNoticeForBookmark: Notice = Notice(), // For navigation to Edit Bookmark from DetailedContent (Since Detailed content requires FullContent to enter, while Edit Bookmark requires Notice to enter. val currentTargetBookmark: Bookmark = Bookmark(-1), val scheduleTriggered: Boolean = false, val languageModelDownloadResult: String = "YET_STARTED" diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt index db20eee3..113a4024 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt @@ -7,11 +7,6 @@ import android.content.Context.NOTIFICATION_SERVICE import android.content.pm.PackageManager import android.util.Log import androidx.core.content.ContextCompat -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.doyoonkim.knutice.R @@ -20,20 +15,16 @@ import com.doyoonkim.knutice.model.NoticeCategory import com.doyoonkim.knutice.model.NotificationPreferenceStatus import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject -val Context.dataStore: DataStore by preferencesDataStore( - name = "notificationPreferences" -) - @HiltViewModel class NotificationPreferenceViewModel @Inject constructor( @ApplicationContext private val context: Context, @@ -50,35 +41,6 @@ class NotificationPreferenceViewModel @Inject constructor( NoticeCategory.EVENT_NEWS to 3 ) - init { - viewModelScope.launch { - // Current Notification Status - context.dataStore.data - .map { Result.success(it) } - .catch { emit(Result.failure(it)) } - .collect { result -> - result.fold( - onSuccess = { - _uiStatus.update { status -> - // TODO: Consider replace data type with MAP - status.copy( - isEachChannelAllowed = listOf( - it[booleanPreferencesKey(NoticeCategory.GENERAL_NEWS.name)] ?: true, - it[booleanPreferencesKey(NoticeCategory.ACADEMIC_NEWS.name)] ?: true, - it[booleanPreferencesKey(NoticeCategory.SCHOLARSHIP_NEWS.name)] ?: true, - it[booleanPreferencesKey(NoticeCategory.EVENT_NEWS.name)] ?: true - ) - ) - } - }, - onFailure = { - Log.d("DataStore", "Unable to fetch Boolean Preferences") - } - ) - } - } - } - // Needed to be refined later. (If user enters this point without initial permission allowance. fun checkMainNotificationPreferenceStatus() { @@ -106,42 +68,59 @@ class NotificationPreferenceViewModel @Inject constructor( } } - fun updateChannelPreference(id: NoticeCategory, status: Boolean) { - viewModelScope.launch { - val channelStatus = List(4) { - if (it == notificationChannels[id]) status - else _uiStatus.value.isEachChannelAllowed[it] - } + fun checkTopicSubscriptionStatus() { + viewModelScope.launch(Dispatchers.IO) { + remoteSource.getTopicSubscriptionStatus() + .fold( + onSuccess = { status -> + Log.d("NotificationPreferenceViewModel", "Status: ${status.body?.toString() ?: "empty"}") + _uiStatus.update { + it.copy( + isEachChannelAllowed = listOf( + status.body?.generalNewsTopic ?: false, + status.body?.academicNewsTopic ?: false, + status.body?.scholarshipNewsTopic ?: false, + status.body?.eventNewsTopic ?: false + ), + isSyncCompleted = true, + isError = false + ) + } + }, + onFailure = { + Log.d("NotificationPreferenceViewModel", "Unable to update preference status.") + _uiStatus.update { + it.copy( + isSyncCompleted = false, + isError = true + ) + } + } + ) + } + } - // Local Status (ViewModel) - launch { - _uiStatus.update { - it.copy( - isEachChannelAllowed = channelStatus - ) - } - } + fun updateChannelPreference(id: NoticeCategory, status: Boolean) { + val updatedStatus = List(_uiStatus.value.isEachChannelAllowed.size) { + if (it == notificationChannels[id]) status + else _uiStatus.value.isEachChannelAllowed[it] + } - // Local Data Store - launch { - context.dataStore.edit { - it[booleanPreferencesKey(id.name)] = status - } - } + _uiStatus.update { + it.copy( + isEachChannelAllowed = updatedStatus + ) + } - // Synchronized with the Preference data on the server. - withContext(Dispatchers.IO) { - remoteSource.submitTopicSubscriptionPreference(id, status) - .fold( - onSuccess = { - Log.d("NotificationPreferenceViewModel", "Request Successful, Result: $it") - }, - onFailure = { - Log.d("NotificationPreferenceViewModel", "Request Failure, REASON: ${it.message}") - } - ) + // Submit updates + CoroutineScope(Dispatchers.IO).launch { + val updateJob = launch { + remoteSource.submitTopicSubscriptionPreference( + id, status + ) } - + delay(5000L) + if (!updateJob.isCompleted) updateJob.cancelAndJoin() } } } \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5ae2cb11..5c691206 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -60,4 +60,5 @@ 言語モデルをダウンロードして翻訳機能を使われます More KNUTICE Notice + 選択したファイルをダウンロード フォルダーにダウンロードします。 \ No newline at end of file diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 949dc6f9..96a1ff1b 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -60,4 +60,5 @@ 번역이 필요하신가요? 한국어 번역 기능을 사용하려면 언어 모델을 다운로드 해야 합니다. + 선택한 파일을 다운로드 폴더에 받고 있어요. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fda8c4c..9e3c6cb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ About Version Open Source License - 1.3.3 + 1.4.0 Notification Preference New Notice has been delivered! @@ -70,4 +70,5 @@ Need translation? English Language model can be downloaded to use live translation + Downloading the selected file to Downloads... \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c7da55f7..aa8fe8b1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,4 @@ - -