diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 48321893..1d6d513a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -7,18 +7,23 @@ plugins {
// Dagger-Hilt for Dependency Injection
id("kotlin-kapt")
- id("com.google.dagger.hilt.android")
+
+ // Keep this plugin in app module, since app module handles all related app information
+ // such as App ID , etc. for using firebase services.
alias(libs.plugins.google.gms.google.services)
alias(libs.plugins.kotlinSerialization)
+ // Required from Kotlin 2.0.0 (Every module using Compose)
+ alias(libs.plugins.compose.compiler)
+
// KSP Plugin for Room Database
// id("com.google.devtools.ksp")
}
android {
namespace = "com.doyoonkim.knutice"
- compileSdk = 34
+ compileSdk = 35
val properties = Properties().apply {
load(FileInputStream("${rootDir}/local.properties"))
@@ -29,8 +34,8 @@ android {
applicationId = "com.doyoonkim.knutice"
minSdk = 31
targetSdk = 34
- versionCode = 16
- versionName = "1.4.1"
+ versionCode = 17
+ versionName = "1.4.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -77,6 +82,14 @@ android {
}
dependencies {
+ implementation(projects.core.model)
+ implementation(projects.core.domain)
+ implementation(projects.core.data)
+ implementation(projects.core.network)
+ implementation(projects.core.notification)
+ implementation(projects.feature.main)
+ implementation(projects.feature.bookmark)
+ implementation(projects.common)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -100,39 +113,23 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
implementation(libs.kotlin.serialization)
// Coroutine for Android
implementation(libs.kotlinx.coroutines.android)
// Navigation for Compose
implementation(libs.androidx.navigation.compose)
- // Coil
- implementation(libs.coil.compose)
-
- // Dagger Hilt for Dependency Injection
- implementation(libs.hilt.android)
- implementation(libs.androidx.hilt.navigation.compose) // With Compose Navigation
- kapt(libs.hilt.android.compiler)
-
- // Retrofit 2
- implementation(libs.retrofit)
- implementation(libs.converter.gson)
-
- // Jsoup HTML Parser Library
- implementation(libs.jsoup)
// DataStore
implementation (libs.androidx.datastore.preferences)
- // Room Database
- implementation(libs.androidx.room.runtime)
- kapt(libs.androidx.room.compiler)
- // Room Database - Kotlin Extensions and Coroutine Support
- implementation(libs.androidx.room.ktx)
-
- // Translation
- implementation(libs.translate)
-
}
// Allow references to generated code
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 61483746..f99b922c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,23 +25,33 @@
android:value="@string/default_notification_channel_id"/>
-
+
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt b/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt
new file mode 100644
index 00000000..e7fddc08
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/AppNavHost.kt
@@ -0,0 +1,53 @@
+package com.doyoonkim.knutice
+
+import android.net.Uri
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import com.doyoonkim.bookmark.bookmarkServiceGraph
+import com.doyoonkim.common.navigation.NavRoutes
+import com.doyoonkim.main.mainServiceNavGraph
+
+@Composable
+fun AppNavHost(
+ modifier: Modifier = Modifier,
+ contentPadding: PaddingValues,
+ navController: NavHostController,
+ viewModelFactory: ViewModelProvider.Factory
+) {
+ NavHost(
+ modifier = modifier.padding(
+ PaddingValues(
+ top = contentPadding.calculateTopPadding(),
+// bottom = contentPadding.calculateBottomPadding()
+ )
+ ),
+ navController = navController,
+ startDestination = NavRoutes.Home.route
+ ) {
+ mainServiceNavGraph(
+ navController = navController,
+ viewModelFactory = viewModelFactory,
+ contentPadding = contentPadding,
+ onNoticeDetailRequested = { target ->
+ navController.navigate("noticeDetail/${target.nttId}/${Uri.encode(target.contentUrl)}/${target.isFabVisible}")
+ },
+ onBookmarkServiceRequested = {
+ navController.navigate("bookmark/${it.noticeId}/${it.noticeTitle}/${it.noticeInfo}")
+ }
+ )
+
+ bookmarkServiceGraph(
+ navController = navController,
+ viewModelFactory = viewModelFactory,
+ contentPadding = contentPadding,
+ onNoticeDetailRequested = { target ->
+ navController.navigate("noticeDetail/${target.nttId}/${Uri.encode(target.contentUrl)}/${target.isFabVisible}")
+ }
+ )
+ }
+}
\ 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
new file mode 100644
index 00000000..d14956ea
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt
@@ -0,0 +1,341 @@
+package com.doyoonkim.knutice
+
+import android.Manifest
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+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.Image
+import androidx.compose.foundation.background
+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.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
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.doyoonkim.common.navigation.NavRoutes
+import com.doyoonkim.common.theme.KNUTICETheme
+import com.doyoonkim.common.theme.containerBackgroundSolid
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
+import com.doyoonkim.common.ui.PermissionRationaleComposable
+import com.doyoonkim.common.R
+import com.doyoonkim.notification.local.NotificationAlarmScheduler
+import javax.inject.Inject
+
+@OptIn(ExperimentalMaterial3Api::class)
+class MainActivity : ComponentActivity() {
+
+ @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
+
+ private val notificationAlarmScheduler by lazy {
+ NotificationAlarmScheduler(this)
+ }
+
+ // NavController
+ private lateinit var navController: NavHostController
+ private val activity = this
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ (applicationContext as MainApplication).appComponent.inject(this)
+ super.onCreate(savedInstanceState)
+
+ val launchedIntent = this.intent
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ enableEdgeToEdge()
+ setContent {
+ KNUTICETheme {
+ val context = LocalContext.current
+ var showPermissionRationale by remember { mutableStateOf(false) }
+ navController = rememberNavController()
+
+ // Bottom Bar Handling
+ var bottomBarState = Triple(true, false, false)
+ val backStackEntryState by navController.currentBackStackEntryAsState()
+ backStackEntryState?.destination?.route.let { route ->
+ bottomBarState = when(route) {
+ NavRoutes.Home.route -> Triple(true, true, false)
+ NavRoutes.Bookmark.route -> Triple(true, false, true)
+ else -> Triple(false, false, false)
+ }
+ }
+
+ // Permission Launcher
+ val requestPermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ permissions.entries.forEach {
+ Log.d("MainServiceScreen", "${it.key}, ${it.value}")
+ if (it.key == Manifest.permission.SCHEDULE_EXACT_ALARM
+ && !notificationAlarmScheduler.canScheduleExactAlarms()) {
+ showPermissionRationale = true
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ // Permission check
+ requestPermissionLauncher.launch(
+ arrayOf(
+ Manifest.permission.POST_NOTIFICATIONS,
+ Manifest.permission.SCHEDULE_EXACT_ALARM
+ )
+ )
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.containerBackgroundSolid),
+ topBar = {
+ TopAppBar(
+ title = {
+ Row(
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (!bottomBarState.first) {
+ IconButton(
+ onClick = {
+ navController.popBackStack()
+ }
+ ) {
+ Image(
+ painter = painterResource(R.drawable.baseline_arrow_back_ios_new_24),
+ contentDescription = "back",
+ modifier = Modifier.wrapContentSize(),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
+ )
+ }
+ }
+
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.app_name),
+ textAlign = TextAlign.Left,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.ExtraBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.title
+ )
+ }
+ },
+ actions = {
+ if (bottomBarState.first) {
+ IconButton(
+ onClick = {
+ navController.navigate(NavRoutes.NoticeSearch.route)
+ }
+ ) {
+ Image(
+ painter = painterResource(R.drawable.baseline_search_24),
+ contentDescription = "Search",
+ modifier = Modifier.wrapContentSize(),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
+ )
+ }
+ IconButton(
+ onClick = {
+ navController.navigate(NavRoutes.Settings.route)
+ }
+ ) {
+ Image(
+ painter = painterResource(R.drawable.baseline_settings_24),
+ contentDescription = "Settings",
+ modifier = Modifier.wrapContentSize(),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
+ )
+ }
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.containerBackgroundSolid,
+ titleContentColor = MaterialTheme.colorScheme.title
+ )
+ )
+ },
+ bottomBar = {
+ if (bottomBarState.first) {
+ BottomAppBar(
+ modifier = Modifier
+ .wrapContentSize()
+ .background(Color.Transparent)
+ .clip(RoundedCornerShape(15.dp)),
+ actions = {
+ // https://developer.android.com/develop/ui/compose/navigation#bottom-nav
+ BottomNavigationItem(
+ selected = bottomBarState.second,
+ enabled = true,
+ onClick = {
+ if (!bottomBarState.second) {
+ navController.navigate(NavRoutes.Home.route) {
+ popUpTo(navController.graph.startDestinationId) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.baseline_home_24),
+ contentDescription = "Main",
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_bar_home))
+ },
+ selectedContentColor = MaterialTheme.colorScheme.title,
+ unselectedContentColor = MaterialTheme.colorScheme.subTitle
+ )
+ BottomNavigationItem(
+ selected = bottomBarState.third,
+ enabled = true,
+ onClick = {
+ if (!bottomBarState.third) {
+ navController.navigate(NavRoutes.Bookmark.route) {
+ popUpTo(navController.graph.startDestinationId) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.baseline_bookmarks_24),
+ contentDescription = "Bookmarks",
+ modifier = Modifier.padding(bottom = 5.dp)
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_bar_bookmark))
+ },
+ selectedContentColor = MaterialTheme.colorScheme.title,
+ unselectedContentColor = MaterialTheme.colorScheme.subTitle
+ )
+ }
+ )
+ }
+ },
+ containerColor = MaterialTheme.colorScheme.containerBackgroundSolid,
+ ) { contentPadding ->
+
+ AppNavHost(
+ modifier = Modifier,
+ contentPadding = contentPadding,
+ navController = navController,
+ viewModelFactory = viewModelFactory
+ )
+
+ LaunchedEffect(Unit) {
+ // Intent handling (access application via onCreate call; click push notification when app is closed.)
+ Log.d("MainActivity", "Intent received: ${launchedIntent?.data}")
+ navController.handleDeepLink(launchedIntent)
+ }
+ }
+
+ AnimatedVisibility(
+ visible = showPermissionRationale,
+ enter = scaleIn(),
+ exit = scaleOut()
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ .clickable { showPermissionRationale = false }
+ ) {
+ PermissionRationaleComposable(
+ modifier = Modifier.align(Alignment.Center).padding(start = 20.dp, end = 20.dp),
+ permissionName = stringResource(R.string.title_alarm_and_reminder),
+ rationaleTitle = stringResource(R.string.text_rationale_title),
+ description = stringResource(R.string.text_rationale_description)
+ ) {
+ val settingIntent = Intent(
+ Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
+ ).apply {
+ this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ this.putExtra(
+ "android.provider.extra.APP_PACKAGE",
+ context.packageName
+ )
+ }
+ context.startActivity(settingIntent)
+ showPermissionRationale = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Called when intent is being sent while the onCreate() is already called.
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+
+ Log.d("MainActivity", "Intent received onNewIntent: ${intent?.data}")
+
+ // Just for extra safety. onNewIntent is called when app receives intent while the onCreate is already being called.
+ // Therefore, it is almost guaranteed that navController is initialized.
+ if (::navController.isInitialized) {
+ navController.handleDeepLink(intent)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ viewModelStore.clear()
+ this.externalCacheDir?.delete()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt b/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt
index abcceceb..91ee85de 100644
--- a/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt
+++ b/app/src/main/java/com/doyoonkim/knutice/MainApplication.kt
@@ -4,16 +4,38 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
-import com.doyoonkim.knutice.fcm.PushNotificationHandler
-import dagger.hilt.android.HiltAndroidApp
+import com.doyoonkim.common.di.AppInjector
+import com.doyoonkim.common.di.AppInjectorProvider
+import com.doyoonkim.common.R
+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 javax.inject.Inject
-@HiltAndroidApp
-class MainApplication() : Application() {
- @Inject lateinit var notificationHandler: PushNotificationHandler
+class MainApplication() : Application(), AppInjectorProvider {
+
+ val appComponent: AppComponent by lazy {
+ DaggerAppComponent.factory().create(this)
+ }
+
+ override val appInjector: AppInjector = object : AppInjector {
+ override fun inject(target: Any) {
+ when(target) {
+ is PushNotificationService -> appComponent.inject(target)
+ else -> error("Unsupported Target $target")
+ }
+ }
+
+ }
+
+ @Inject lateinit var tokenHandler: TokenHandler
override fun onCreate() {
super.onCreate()
+ // Application-Level injection
+ appComponent.inject(this)
+
// Create channel group
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).run {
createNotificationChannel(
@@ -22,7 +44,7 @@ class MainApplication() : Application() {
getString(R.string.inapp_notification_channel_description)
)
}
- notificationHandler.requestCurrentToken()
+ tokenHandler.handleCurrentTokenRequest()
}
override fun onTerminate() {
diff --git a/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmReceiver.kt b/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmReceiver.kt
deleted file mode 100644
index c680d2b2..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmReceiver.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.doyoonkim.knutice.alarm
-
-import android.app.NotificationManager
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-
-class AlarmReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- context?.let {
- val notificationManager =
- it.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- val runnerNotifier = RunnerNotifier(notificationManager, context)
- runnerNotifier.showNotification(intent?.getStringExtra("note")?: "")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmScheduler.kt b/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmScheduler.kt
deleted file mode 100644
index e179ff44..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/alarm/AlarmScheduler.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.doyoonkim.knutice.alarm
-
-import android.app.PendingIntent
-import com.doyoonkim.knutice.model.Bookmark
-
-
-/**
- * Alarm feature (Part of the Bookmark)
- * Reference: https://medium.com/@tolgapirim25/send-notifications-at-a-specific-time-with-alarm-manager-on-android-13c7cc9d8e7a
- */
-interface AlarmScheduler {
- fun createPendingIntent(target: Bookmark): PendingIntent
-
- fun schedule(target: Bookmark)
-
- fun cancel(target: Bookmark)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/alarm/NotificationAlarmScheduler.kt b/app/src/main/java/com/doyoonkim/knutice/alarm/NotificationAlarmScheduler.kt
deleted file mode 100644
index 8fcaf806..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/alarm/NotificationAlarmScheduler.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.doyoonkim.knutice.alarm
-
-import android.app.AlarmManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.util.Log
-import com.doyoonkim.knutice.model.Bookmark
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-
-class NotificationAlarmScheduler @Inject constructor(
- @ApplicationContext private val context: Context
-) : AlarmScheduler {
- // AlarmManager Instance
- private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
-
- override fun createPendingIntent(target: Bookmark): PendingIntent {
- val intent = Intent(context, AlarmReceiver::class.java).apply {
- putExtra("note", target.note)
- }
-
- return PendingIntent.getBroadcast(
- context,
- target.bookmarkId,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- }
-
- override fun schedule(target: Bookmark) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (!alarmManager.canScheduleExactAlarms()) {
- Log.d("NotificationAlarmScheduler", "Unable")
- return
- }
- }
- Log.d("NotificationAlarmSchedule", "Would be scheduled")
- alarmManager.setExact(
- AlarmManager.RTC_WAKEUP,
- target.reminderSchedule,
- createPendingIntent(target)
- )
-
- Log.d("NotificationAlarmSchedule", "Scheduled")
- }
-
- override fun cancel(target: Bookmark) {
- alarmManager.cancel(
- createPendingIntent(target)
- )
- }
-
- fun canScheduleExactAlarms(): Boolean {
- return alarmManager.canScheduleExactAlarms()
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/alarm/RunnerNotifier.kt b/app/src/main/java/com/doyoonkim/knutice/alarm/RunnerNotifier.kt
deleted file mode 100644
index ed7a809c..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/alarm/RunnerNotifier.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.doyoonkim.knutice.alarm
-
-import android.app.Notification
-import android.app.NotificationManager
-import android.content.Context
-import androidx.core.app.NotificationCompat
-import com.doyoonkim.knutice.R
-import kotlin.random.Random
-
-class RunnerNotifier(
- private val notificationManager: NotificationManager,
- private val context: Context,
-) : Notifier(notificationManager) {
- override val channelId: String = context.getString(R.string.inapp_notification_channel_id)
- override val channelName: String = context.getString(R.string.inapp_notificaiton_channel_name)
- override val notificationId = Random(System.currentTimeMillis()).nextInt()
-
- override fun buildNotification(note: String): Notification {
- return NotificationCompat.Builder(context, channelId)
- .setContentTitle(context.getString(R.string.text_reminder_title))
- .setContentText(note)
- .setSmallIcon(R.mipmap.ic_launcher)
- .build()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/data/KnuticeRemoteSource.kt b/app/src/main/java/com/doyoonkim/knutice/data/KnuticeRemoteSource.kt
deleted file mode 100644
index 7766063e..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/data/KnuticeRemoteSource.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-package com.doyoonkim.knutice.data
-
-import android.util.Log
-import com.doyoonkim.knutice.model.ApiDeviceTokenRequest
-import com.doyoonkim.knutice.model.DeviceTokenRequest
-import com.doyoonkim.knutice.model.NoticeCategory
-import com.doyoonkim.knutice.model.NoticesPerPage
-import com.doyoonkim.knutice.model.ApiPostResult
-import com.doyoonkim.knutice.BuildConfig
-import com.doyoonkim.knutice.model.ApiReportRequest
-import com.doyoonkim.knutice.model.ApiTopicSubscriptionRequest
-import com.doyoonkim.knutice.model.ManageTopicRequest
-import com.doyoonkim.knutice.model.SingleNotice
-import com.doyoonkim.knutice.model.ReportRequest
-import com.doyoonkim.knutice.model.TopicSubscriptionStatusDTO
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import org.jsoup.Jsoup
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
-import retrofit2.http.Body
-import retrofit2.http.GET
-import retrofit2.http.Header
-import retrofit2.http.Headers
-import retrofit2.http.POST
-import retrofit2.http.Path
-import retrofit2.http.Query
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class KnuticeRemoteSource @Inject constructor() {
-
- private val knuticeService = Retrofit.Builder()
- .baseUrl(BuildConfig.API_MIGRATED)
- .addConverterFactory(GsonConverterFactory.create())
- .build()
-
- // TODO: Should relocate this variable.
- private var validatedToken: String = ""
-
- suspend fun getTopThreeNotice(category: NoticeCategory, size: Int): NoticesPerPage {
- return knuticeService.create(KnuticeService::class.java).run {
- this.getTopThreeNotice(category, size)
- }
- }
-
- suspend fun getNoticeListPerPage(category: NoticeCategory, lastNttId: Int): NoticesPerPage {
- Log.d("KnuticeRemoteSource", "Start retrofit service")
- return knuticeService.create(KnuticeService::class.java).run {
- if (lastNttId == 0) {
- this.getFirstPageOfNotice(category)
- } else {
- this.getNoticeListPerPage(category, lastNttId)
- }
- }
- }
-
- suspend fun getNoticeById(id: String): Result {
- 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)
- .get()
- .getElementsByClass("bbs-view-content bbs-view-content-skin05")
- .text() ?: "Unable to receive full notice content"
- }
-
- suspend fun validateToken(token: String): Result {
- Log.d("KnuticeRemoteSource", "Token Provided: $token")
- try {
- knuticeService.create(KnuticeService::class.java).validateToken(
- ApiDeviceTokenRequest(body = DeviceTokenRequest(token))
- ).run {
- if (this.result?.resultCode == 200) {
- Log.d("KnuticeServer", "Token saved.")
- validatedToken = token
- return Result.success(true)
- } else {
- Log.d("KnuticeServer", "Failed to save token")
- return Result.success(false)
- }
- }
- } catch (e: Exception) {
- Log.d("KnuticeServer", "Failed to validate token\nREASON:${e.message}")
- return Result.failure(e)
- }
- }
-
- suspend fun submitUserReport(report: ReportRequest): Result {
- Log.d("KnuticeRemoteSource", "ValidatedToken: $validatedToken")
- try {
- knuticeService.create(KnuticeService::class.java).submitUserReport(
- ApiReportRequest(body = report.copy(fcmToken = validatedToken))
- ).run {
- if (this.result?.resultCode == 200) {
- Log.d("KnuticeServer", "User report has been submitted successfully.\n${this.body}")
- return Result.success(true)
- } else {
- Log.d("KnuticeServer", "Failed to submit user report\n${this.body ?: false}")
- return Result.success(false)
- }
- }
- } catch (e: Exception) {
- Log.d("KnuticeServer", "Failed to submit user report.\nREASON: ${e.message}")
- return Result.failure(e)
- }
- }
-
- suspend fun submitTopicSubscriptionPreference(topic: NoticeCategory, status: Boolean): Result {
- Log.d("KnuticeRemoteSource", "Update Topic Subscription Preference")
- try {
- knuticeService.create(KnuticeService::class.java).submitTopicSubscriptionPreference(
- ApiTopicSubscriptionRequest(
- body = ManageTopicRequest(validatedToken, topic.name, status)
- )
- ).run {
- if (this.result?.resultCode == 200) {
- Log.d("KnuticeServer", "Topic preference has been updated.\n${this.body}")
- return Result.success(true)
- } else {
- Log.d("KnuticeServer", "Failed to update topic preference.\n${this.body ?: false}")
- return Result.success(false)
- }
- }
- } catch (e: Exception) {
- Log.d("KnuticeServer", "Failed to submit user report. \nREASON: ${e.message}")
- return Result.failure(e)
- }
- }
-
-}
-
-interface KnuticeService {
-
- @GET("open-api/notice/list")
- suspend fun getTopThreeNotice(
- @Query("noticeName") category: NoticeCategory,
- @Query("size") size: Int
- ): NoticesPerPage
-
- @GET("open-api/notice/list")
- suspend fun getNoticeListPerPage(
- @Query("noticeName") category: NoticeCategory,
- @Query("nttId") lastNttId: Int
- ): NoticesPerPage
-
- @GET("open-api/notice/list")
- suspend fun getFirstPageOfNotice(
- @Query("noticeName") category: NoticeCategory
- ): NoticesPerPage
-
- @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")
- suspend fun validateToken(
- @Body requestBody: ApiDeviceTokenRequest
- ): ApiPostResult
-
- @Headers("Content-Type: application/json")
- @POST("open-api/report")
- suspend fun submitUserReport(
- @Body requestBody: ApiReportRequest
- ): ApiPostResult
-
- @Headers("Content-Type: application/json")
- @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/data/NoticeLocalRepository.kt b/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt
deleted file mode 100644
index 7bd060ae..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.doyoonkim.knutice.data
-
-import androidx.annotation.WorkerThread
-import com.doyoonkim.knutice.data.local.KnuticeLocalSource
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import com.doyoonkim.knutice.model.NoticeEntity
-import com.doyoonkim.knutice.model.NoticesPerPage
-import dagger.hilt.android.scopes.ActivityRetainedScoped
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
-import javax.inject.Inject
-
-/*
-ActivityRetainedComponent lives across configuration changes, so it is created at the first onCreate
- and last onDestroy, and when you mark your dependencies in ActivityRetainedComponent with
- @ActivityRetainedScope its guarantees that your object will be a singleton and survive across
- configuration changes
- */
-// TODO: Consider change class name to KnuticeLocalRepository
-@ActivityRetainedScoped
-class NoticeLocalRepository @Inject constructor(
- private val remoteSource: KnuticeRemoteSource,
- private val localSource: KnuticeLocalSource
-) {
- // Local
- fun createBookmark(bookmark: Bookmark): Result {
- return localSource.createBookmark(bookmark)
- }
-
- fun createBookmark(bookmark: Bookmark, targetNotice: Notice): Result {
- return localSource.createBookmark(bookmark, targetNotice)
- }
-
- fun updateBookmark(bookmark: Bookmark) {
- localSource.updateBookmark(bookmark)
- }
-
- fun deleteBookmark(bookmark: Bookmark) {
- localSource.deleteBookmark(bookmark)
- }
-
- fun deleteNoticeEntity(entity: NoticeEntity) {
- localSource.deleteNoticeEntity(entity)
- }
-
- fun getAllBookmarks(): Flow {
- return flow {
- runCatching {
- localSource.getAllBookmarks().forEach { emit(it) }
- }.onFailure { emit(Bookmark(-1)) }
- }
- }
-
- fun getNoticeByNttId(nttId: Int): Notice {
- val noticeEntity = localSource.getNoticeByNttId(nttId)
- return noticeEntity.toNotice()
- }
-
- fun getBookmarkByNttID(nttId: Int): Result {
- return runCatching {
- localSource.getBookmarkByNttId(nttId) ?: Bookmark(-1)
- }.onFailure { throw it }
- }
-
- // Remote
- @WorkerThread
- fun getTopThreeNotice(category: NoticeCategory): Flow {
- return flow {
- delay(10L)
- val response = remoteSource.getTopThreeNotice(category, 3)
-
- if (response.result?.resultCode == 200) emit(response)
- else emit(NoticesPerPage())
- }.flowOn(Dispatchers.IO)
- }
-
- fun getNoticesByCategoryPerPage(category: NoticeCategory, lastNttId: Int): Flow {
- return flow {
- val response = remoteSource.getNoticeListPerPage(category, lastNttId)
- if (response.result?.resultCode == 200) {
- emit(response)
- } else {
- NoticesPerPage()
- }
- }.flowOn(Dispatchers.IO)
- }
-
- fun queryNoticesByKeyword(keyword: String): Flow {
- return flow {
- remoteSource.queryNoticesByKeyword(keyword).run {
- if (this.result?.resultCode == 200) {
- emit(this)
- } else {
- emit(NoticesPerPage())
- }
- }
- }.flowOn(Dispatchers.IO)
- }
-
- fun getFullNoticeContent(url: String): Flow {
- return flow {
- emit(remoteSource.getFullNoticeContent(url).await())
- }.flowOn(Dispatchers.IO)
- }
-
- fun postNoticeSubscriptionPreference(topic: NoticeCategory, status: Boolean): Flow> {
- return flow {
- emit(remoteSource.submitTopicSubscriptionPreference(
- topic, status
- ))
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt b/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt
deleted file mode 100644
index 95ac77e0..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.doyoonkim.knutice.data.local
-
-import android.content.Context
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeEntity
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-
-
-class KnuticeLocalSource @Inject constructor(
- @ApplicationContext private val context: Context
-) {
- // Local Database
- private val localDatabase: LocalDatabase by lazy {
- LocalDatabase.getInstance(context)
- }
-
- // CURD
- fun createBookmark(bookmark: Bookmark): Result {
- return try {
- localDatabase.getDao().createBookmark(bookmark)
- Result.success(true)
- } catch (e: Exception) {
- Result.failure(e)
- }
- }
-
- fun createBookmark(bookmark: Bookmark, targetNotice: Notice): Result {
- return runCatching {
- // Insert Notice First.
- localDatabase.getDao().createNoticeEntity(targetNotice.toNoticeEntity())
- // Insert Bookmark Entity
- localDatabase.getDao().createBookmark(bookmark)
- true
- }.onFailure { throw it }
- }
-
- fun updateBookmark(bookmark: Bookmark): Result {
- return runCatching {
- localDatabase.getDao().updateBookmark(bookmark)
- true
- }.onFailure { throw it }
- }
-
- fun getAllBookmarks(): List {
- return localDatabase.getDao().getAllBookmarks()
- }
-
- fun deleteBookmark(bookmark: Bookmark): Result {
- return runCatching {
- localDatabase.getDao().deleteBookmark(bookmark)
- true
- }.onFailure { throw it }
- }
-
- fun deleteNoticeEntity(entity: NoticeEntity): Result {
- return runCatching {
- localDatabase.getDao().deleteNoticeEntity(entity)
- true
- }.onFailure { throw it }
- }
-
- fun getNoticeByNttId(nttId: Int): NoticeEntity {
- return localDatabase.getDao().getNoticeByNttId(nttId)
- }
-
- fun getBookmarkByNttId(nttId: Int): Bookmark? {
- return localDatabase.getDao().getBookmarkByNttId(nttId)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/data/local/LocalRoomDatabase.kt b/app/src/main/java/com/doyoonkim/knutice/data/local/LocalRoomDatabase.kt
deleted file mode 100644
index fd42cf56..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/data/local/LocalRoomDatabase.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.doyoonkim.knutice.data.local
-
-import android.content.Context
-import androidx.room.Database
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.NoticeEntity
-
-
-@Database(entities = [Bookmark::class, NoticeEntity::class], version = 1)
-abstract class LocalDatabase : RoomDatabase() {
- abstract fun getDao(): MainDatabaseDao
-
- companion object {
- private var INSTANCE: LocalDatabase? = null
-
- fun getInstance(context: Context): LocalDatabase {
- if (INSTANCE == null) {
- synchronized(LocalDatabase::class) {
- INSTANCE = Room.databaseBuilder(
- context.applicationContext,
- LocalDatabase::class.java,
- "Main Local Database"
- ).build()
- }
- }
- return INSTANCE!!
- }
- }
-}
diff --git a/app/src/main/java/com/doyoonkim/knutice/di/AppComponent.kt b/app/src/main/java/com/doyoonkim/knutice/di/AppComponent.kt
new file mode 100644
index 00000000..7d49f350
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/di/AppComponent.kt
@@ -0,0 +1,47 @@
+package com.doyoonkim.knutice.di
+
+import android.app.Application
+import com.doyoonkim.bookmark.di.BookmarkModule
+import com.doyoonkim.common.di.CommonModule
+import com.doyoonkim.data.di.DataModule
+import com.doyoonkim.domain.di.DomainModule
+import com.doyoonkim.knutice.MainActivity
+import com.doyoonkim.knutice.MainApplication
+import com.doyoonkim.main.di.MainModule
+import com.doyoonkim.notification.di.NotificationModule
+import com.doyoonkim.notification.fcm.PushNotificationService
+import dagger.BindsInstance
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+ modules = [
+ AppModule::class,
+ CommonModule::class,
+ DataModule::class,
+ DomainModule::class,
+ NotificationModule::class,
+ BookmarkModule::class,
+ MainModule::class,
+ ViewModelFactoryModule::class
+ ]
+)
+interface AppComponent {
+
+ fun inject(app: MainApplication)
+
+ fun inject(activity: MainActivity)
+
+ fun inject(service: PushNotificationService)
+
+ @Component.Factory
+ interface Factory {
+ // provide AppComponent
+ // @BindsInstance would bind provided 'Application' to DI graph.
+ // Therefore, Application instance would be bind alongside with AppComponent creation.
+ // --> This would provide 'Context' to DI graph as well because Application is a context.
+ fun create(@BindsInstance application: Application): AppComponent
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/di/AppModule.kt b/app/src/main/java/com/doyoonkim/knutice/di/AppModule.kt
new file mode 100644
index 00000000..b9f6184b
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/di/AppModule.kt
@@ -0,0 +1,19 @@
+package com.doyoonkim.knutice.di
+
+import android.app.Application
+import android.content.Context
+import com.doyoonkim.common.di.ApplicationContext
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+object AppModule {
+
+ // Inject ApplicationContext
+ @Provides
+ @Singleton
+ @ApplicationContext
+ fun providesApplicationContext(app: Application): Context = app.applicationContext
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/di/DaggerViewModelFactory.kt b/app/src/main/java/com/doyoonkim/knutice/di/DaggerViewModelFactory.kt
new file mode 100644
index 00000000..04f94ab2
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/di/DaggerViewModelFactory.kt
@@ -0,0 +1,20 @@
+package com.doyoonkim.knutice.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+import javax.inject.Provider
+
+class DaggerViewModelFactory @Inject constructor(
+ private val providers: @JvmSuppressWildcards Map, Provider>
+) : ViewModelProvider.Factory {
+ // Function to lazily creates ViewModel instance
+ override fun create(modelClass: Class): T {
+ val provider = providers[modelClass]
+ ?: providers.entries.firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
+ ?:throw IllegalArgumentException("Unknown ViewModel Class $modelClass")
+
+ @Suppress("UNCHECKED_CAST")
+ return provider.get() as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/di/ViewModelFactoryModule.kt b/app/src/main/java/com/doyoonkim/knutice/di/ViewModelFactoryModule.kt
new file mode 100644
index 00000000..41c564bf
--- /dev/null
+++ b/app/src/main/java/com/doyoonkim/knutice/di/ViewModelFactoryModule.kt
@@ -0,0 +1,13 @@
+package com.doyoonkim.knutice.di
+
+import androidx.lifecycle.ViewModelProvider
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class ViewModelFactoryModule {
+
+ // @Binds annotation instructs Dagger to use the provided implementation.
+ @Binds
+ abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt
deleted file mode 100644
index 8192a732..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.model.DetailedContentState
-import kotlinx.coroutines.flow.Flow
-
-interface CrawlFullContent {
-
- fun getFullContentFromSource(title: String, info: String, url: String): Flow
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt
deleted file mode 100644
index ce97184e..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.DetailedContentState
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-class CrawlFullContentImpl @Inject constructor(
- private val repository: NoticeLocalRepository
-): CrawlFullContent {
- override fun getFullContentFromSource(
- title: String,
- info: String,
- url: String
- ): Flow {
- return repository.getFullNoticeContent(url)
- .map {
- DetailedContentState(
- title = title,
- info = info,
- fullContent = it,
- fullContentUrl = url
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt
deleted file mode 100644
index fe17d422..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.Notice
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-class FetchBookmarkFromDatabase @Inject constructor(
- private val repository: NoticeLocalRepository
-) {
-
- fun getAllBookmarkWithNotices(): Flow> {
- return repository.getAllBookmarks().map {
- Pair(it, repository.getNoticeByNttId(it.nttId))
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchListOfNotices.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchListOfNotices.kt
deleted file mode 100644
index 7f9aef2d..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchListOfNotices.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import com.doyoonkim.knutice.model.RawNoticeData
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-interface FetchListOfNotices {
-
- fun getNoticesPerPage(category: NoticeCategory, lastNttId: Int): Flow>
-
- fun getNoticesByKeyword(keyword: String): Flow>
-
- fun ArrayList.toNotice(): List {
- return List(this.size) { index ->
- Notice(
- nttId = this[index].nttId ?: -1,
- title = this[index].title ?: "Unknown",
- url = this[index].contentUrl ?: "Unknown",
- imageUrl = this[index].contentImage ?: "Unknown",
- departName = this[index].departName ?: "Unknown",
- timestamp = this[index].registeredAt ?: "Unknown"
- )
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchNoticesPerPageInCategory.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchNoticesPerPageInCategory.kt
deleted file mode 100644
index 82a43812..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchNoticesPerPageInCategory.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-class FetchNoticesPerPageInCategory @Inject constructor(
- private val repository: NoticeLocalRepository
-) : FetchListOfNotices {
-
- override fun getNoticesPerPage(category: NoticeCategory, lastNttId: Int): Flow> {
- return repository.getNoticesByCategoryPerPage(category, lastNttId).map {
- it.body.toNotice()
- }
- }
-
- override fun getNoticesByKeyword(keyword: String): Flow> {
- TODO("Does not required to be implemented.")
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt
deleted file mode 100644
index 7afc319f..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchSingleNoticeImpl.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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/domain/FetchTopThreeNotice.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNotice.kt
deleted file mode 100644
index b7ed09ca..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNotice.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import kotlinx.coroutines.flow.Flow
-
-interface FetchTopThreeNotice {
-
- fun fetchTopThreeGeneralNotice(): Flow
-
- fun fetchTopThreeAcademicNotice(): Flow
-
- fun fetchTopThreeScholarshipNotice(): Flow
-
- fun fetchTopThreeEventNotice(): Flow
-
- fun fetchAllTopThreeNotice(): Flow>
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt
deleted file mode 100644
index 3ebb3c2f..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import android.util.Log
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import com.doyoonkim.knutice.model.RawNoticeData
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import java.util.Locale
-import javax.inject.Inject
-
-
-class FetchTopThreeNoticeByCategory @Inject constructor (
- private val repository: NoticeLocalRepository
-): FetchTopThreeNotice {
-
- override fun fetchTopThreeGeneralNotice(): Flow {
- return flowOf(TopThreeInCategory(false))
- }
-
- override fun fetchTopThreeAcademicNotice(): Flow {
- return flowOf(TopThreeInCategory(false))
- }
-
- override fun fetchTopThreeScholarshipNotice(): Flow {
- return flowOf(TopThreeInCategory(false))
- }
-
- override fun fetchTopThreeEventNotice(): Flow {
- return flowOf(TopThreeInCategory(false))
- }
-
- override fun fetchAllTopThreeNotice(): Flow> {
- TODO("Not yet implemented")
- }
-
- fun getTopThreeNotices(category: NoticeCategory): Flow {
- val translateNeeded: Boolean = Locale.getDefault() != Locale.KOREAN
- return repository.getTopThreeNotice(category).map {
- if (it.body.isNotEmpty()) {
- val result = it.body.toNotice()
-
- TopThreeInCategory(
- isSuccessful = true,
- notice1 = result[0],
- notice2 = result[1],
- notice3 = result[2]
- ).also { Log.d("TEST", it.toString()) }
- } else {
- TopThreeInCategory(
- isSuccessful = false
- )
- }
- }
- }
-
- private fun ArrayList.toNotice(): List {
- return List(3) { index ->
- Notice(
- nttId = this[index].nttId ?: -1,
- title = this[index].title ?: "Unknown",
- url = this[index].contentUrl ?: "Unknown",
- departName = this[index].departName ?: "Unknown",
- timestamp = this[index].registeredAt ?: "Unknown",
- imageUrl = this[index].contentImage ?: "Unknown"
- )
- }
- }
-
-}
-
-data class TopThreeInCategory(
- val isSuccessful: Boolean,
- val notice1: Notice? = null,
- val notice2: Notice? = null,
- val notice3: Notice? = null
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/NoticeDummySource.kt b/app/src/main/java/com/doyoonkim/knutice/domain/NoticeDummySource.kt
deleted file mode 100644
index 6479188d..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/NoticeDummySource.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import android.util.Log
-import com.doyoonkim.knutice.model.RawNoticeData
-import com.doyoonkim.knutice.model.Result
-import com.doyoonkim.knutice.model.TopThreeNotices
-import kotlinx.coroutines.delay
-
-class NoticeDummySource {
- companion object {
- /**
- * TEST ONLY (Should be removed later)
- */
- suspend fun getTopThreeNoticeDummy(): TopThreeNotices {
- Log.d("KnuticeRemoteSource", "Dummy Data Created")
- delay(1000)
- return TopThreeNotices(
- result = Result(
- resultCode = 200,
- resultMessage = "Dummy Data",
- resultDescription = "DummyData Created"
- ),
- body = TopThreeNotices.Body(
- latestThreeGeneralNews = arrayListOf(
- RawNoticeData(
- 1, "General 0", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 2, "General 1", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 3, "General 2", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- )
- ),
- latestThreeAcademicNews = arrayListOf(
- RawNoticeData(
- 1, "Academic 0", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 2, "Academic 1", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 3, "Academic 2", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- )
- ),
- latestThreeScholarshipNews = arrayListOf(
- RawNoticeData(
- 1, "Scholarship 0", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 2, "Scholarship 1", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 3, "Scholarship 2", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- )
- ),
- latestThreeEventNews = arrayListOf(
- RawNoticeData(
- 1, "Event 0", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 2, "Event 1", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- ),
- RawNoticeData(
- 3, "Event 2", "https:/www.ut.ac.kr/", "Dept", "2024-00-00"
- )
- )
- )
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/QueryNoticesUsingKeyword.kt b/app/src/main/java/com/doyoonkim/knutice/domain/QueryNoticesUsingKeyword.kt
deleted file mode 100644
index ec242988..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/domain/QueryNoticesUsingKeyword.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.doyoonkim.knutice.domain
-
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-class QueryNoticesUsingKeyword @Inject constructor(
- private val repository: NoticeLocalRepository
-): FetchListOfNotices {
-
- override fun getNoticesPerPage(category: NoticeCategory, lastNttId: Int): Flow> {
- TODO("Does not required to be implemented.")
- }
-
- override fun getNoticesByKeyword(keyword: String): Flow> {
- return repository.queryNoticesByKeyword(keyword).map {
- it.body.toNotice()
- }
- }
-}
\ 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
deleted file mode 100644
index 6a6e3753..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/fcm/PushNotificationHandler.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-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
-import com.google.firebase.messaging.RemoteMessage
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-import kotlin.random.Random
-
-class PushNotificationHandler @Inject constructor() : FirebaseMessagingService() {
- @Inject lateinit var remoteSource: KnuticeRemoteSource
- private val TAG = "PushNotificationHandler"
-
- override fun onNewToken(token: String) {
- super.onNewToken(token)
-
- // POST request to send FCM Token to the Server.
- Log.d(TAG, "Received Token: ${token.toString()}")
- }
-
- 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}") // message.data: Map
-
- // 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()
- }
- }
- }
-
- private fun RemoteMessage.toPushNotification() {
- // Create Pending Intent (For access push notification while the app is in foreground)
- val intent = Intent(applicationContext, MainActivity::class.java).apply {
- this@toPushNotification.data.forEach {
- putExtra(it.key, it.value)
- }
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
- val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
-
- val notificationId = Random(System.currentTimeMillis().toInt()).nextInt()
- // Utilize channel already created by FCM as default
- val notificationBuilder = NotificationCompat.Builder(
- applicationContext, getString(R.string.inapp_notification_channel_id)
- ).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.")
- setContentIntent(pendingIntent)
- setPriority(NotificationCompat.PRIORITY_DEFAULT)
- setAutoCancel(true)
- }
-
-
- with(NotificationManagerCompat.from(applicationContext)) {
- if (ActivityCompat.checkSelfPermission(
- applicationContext,
- Manifest.permission.POST_NOTIFICATIONS
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- Log.d("NotificationHandler", "Permission Denied")
- // TODO: Consider calling
- // ActivityCompat#requestPermissions
- // here to request the missing permissions, and then overriding
- // public void onRequestPermissionsResult(int requestCode, String[] permissions,
- // int[] grantResults)
- // to handle the case where the user grants the permission. See the documentation
- // for ActivityCompat#requestPermissions for more details.
- return
- }
- notify(notificationId, notificationBuilder.build())
- }
- }
-
- fun requestCurrentToken() {
- 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")
-
- // POST request to upload current token to the web server.
- CoroutineScope(Dispatchers.IO).launch {
- remoteSource.validateToken(registrationToken)
- }
- })
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt b/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt
deleted file mode 100644
index 9a6a9575..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-package com.doyoonkim.knutice.model
-
-import com.google.gson.annotations.SerializedName
-import kotlinx.serialization.Serializable
-
-
-data class Result(
- @SerializedName("resultCode") var resultCode: Int? = null,
- @SerializedName("resultMessage") var resultMessage: String? = null,
- @SerializedName("resultDescription") var resultDescription: String? = null
-)
-
-data class RawNoticeData(
- @SerializedName("nttId") var nttId: Int? = null,
- @SerializedName("title") var title: String? = null,
- @SerializedName("contentUrl") var contentUrl: String? = null,
- @SerializedName("contentImage") var contentImage: String? = null,
- @SerializedName("departmentName") var departName: String? = null,
- @SerializedName("registeredAt") var registeredAt: String? = null,
- @SerializedName("noticeName") var noticeCategory: String? = null
-)
-
-// POJO for receiving raw data from the server.
-data class TopThreeNotices(
- @SerializedName("result" ) var result : Result? = Result(),
- @SerializedName("body" ) var body : Body? = Body()
-) {
- data class Body(
- @SerializedName("latestThreeGeneralNews") var latestThreeGeneralNews: ArrayList = arrayListOf(),
- @SerializedName("latestThreeScholarshipNews") var latestThreeScholarshipNews: ArrayList = arrayListOf(),
- @SerializedName("latestThreeEventNews") var latestThreeEventNews: ArrayList = arrayListOf(),
- @SerializedName("latestThreeAcademicNews") var latestThreeAcademicNews: ArrayList = arrayListOf()
- )
-}
-
-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()
-)
-
-data class ApiPostResult(
- @SerializedName("result") var result: Result? = Result(),
- @SerializedName("body") var body: Boolean? = null
-)
-
-data class ApiDeviceTokenRequest(
- val result: Result = Result(),
- val body: DeviceTokenRequest
-)
-
-data class DeviceTokenRequest(
- val fcmToken: String
-)
-
-data class ApiReportRequest(
- val result: Result = Result(),
- val body: ReportRequest
-)
-
-data class ReportRequest(
- val fcmToken: String = "",
- val content: String = "",
- val clientType: String = "APP",
- val deviceName: String = "",
- val version: String = ""
-)
-
-data class ApiTopicSubscriptionRequest(
- val result: Result = Result(),
- val body: ManageTopicRequest = ManageTopicRequest()
-)
-
-data class ManageTopicRequest(
- val fcmToken: String = "",
- val noticeName: String = "",
- val isSubscribed: Boolean = false
-)
-
-// Data class to be applied to uiState.
-// Universal
-@Serializable
-data class Notice(
- val nttId: Int = -1,
- val title: String = "Unknown",
- val url: String = "Unknown",
- val imageUrl: String = "",
- val departName: String = "Unknown",
- val timestamp: String = "Unknown"
-) {
- fun toFullContent(): FullContent {
- return FullContent(
- title,
- "[$departName] $timestamp",
- url,
- imageUrl,
- nttId.toString()
- )
- }
-
- fun toNoticeEntity(): NoticeEntity {
- return NoticeEntity(
- noticeEntityId = 0,
- nttId = nttId,
- title = title,
- url = url,
- imageUrl = imageUrl,
- departName = departName,
- timestamp = timestamp
- )
- }
-}
-
-// DetailedNoticeContent
-data class DetailedContentState(
- val requestedNotice: Notice = Notice(),
- val url: String = "",
- val title: String = "",
- val info: String = "",
- val fullContent: String = "",
- val fullContentUrl: String = "",
- val imageUrl: String = "",
- val loadingStatue: Float = 0.0f
-)
-
-// CustomerService
-data class CustomerServiceReportState(
- val userReport: String = "",
- val reachedMaxCharacters: Boolean = false,
- val exceedMinCharacters: Boolean = false,
- val isSubmissionFailed: Boolean = false,
- val isSubmissionCompleted: Boolean = false
-)
-
-// Search
-data class SearchNoticeState(
- val searchKeyword: String = "",
- val isQuerying: Boolean = false,
- val queryResult: List = emptyList()
-)
-
-// NotificationPreference
-data class NotificationPreferenceStatus(
- val isMainNotificationPermissionGranted: Boolean = false,
- //TODO: Consider change data type to MAP
- val isEachChannelAllowed: List = listOf(false, false, false, false),
- val isSyncCompleted: Boolean = false,
- val isError: Boolean = false
-)
-
-// BookmarkComposable
-data class BookmarkComposableState(
- val bookmarks: List> = emptyList(),
- val isRefreshing: Boolean = false,
- val isRefreshRequested: Boolean = false
-)
-
-// EditBookmark
-data class EditBookmarkState(
- val bookmarkId: Int = 0,
- val targetNotice: Notice = Notice(),
- val isReminderRequested: Boolean = false,
- val timeForRemind: Long = 0, // Should be replaced with an appropriate data type later.
- val bookmarkNote: String = "",
- val requireCreation: Boolean = true,
- val bookmarkInstance: Bookmark? = null,
- val datePickerVisible: Boolean = false
-)
-
diff --git a/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt b/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt
deleted file mode 100644
index 1c18f1ea..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.doyoonkim.knutice.model
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity
-data class NoticeEntity(
- @PrimaryKey(autoGenerate = true) val noticeEntityId: Int,
- @ColumnInfo("ntt_id") val nttId: Int = -1,
- @ColumnInfo("notice_title") val title: String,
- @ColumnInfo("notice_url") val url: String,
- @ColumnInfo("notice_image") val imageUrl: String,
- @ColumnInfo("info_dept") val departName: String,
- @ColumnInfo("info_timestamp") val timestamp: String
-) {
- fun toNotice(): Notice {
- return Notice(nttId, title, url, imageUrl, departName, timestamp)
- }
-}
-
-@Entity
-data class Bookmark(
- @PrimaryKey(autoGenerate = true) val bookmarkId: Int,
- @ColumnInfo("isScheduled") val isScheduled: Boolean = false,
- @ColumnInfo("remind_schedule") val reminderSchedule: Long = 0,
- @ColumnInfo("bookmark_note") val note: String = "",
- @ColumnInfo("target_ntt_id") val nttId: Int = -1
-)
-
-/*
-data class Notice(
- val nttId: Int = -1,
- val title: String = "Unknown",
- val url: String = "Unknown",
- val imageUrl: String = "",
- val departName: String = "Unknown",
- val timestamp: String = "Unknown"
-)
- */
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt b/app/src/main/java/com/doyoonkim/knutice/model/Types.kt
deleted file mode 100644
index bb2f99e7..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.doyoonkim.knutice.model
-
-import kotlinx.serialization.Serializable
-
-enum class NoticeCategory { GENERAL_NEWS, ACADEMIC_NEWS, SCHOLARSHIP_NEWS, EVENT_NEWS, Unspecified }
-
-enum class Destination { MAIN, MORE_GENERAL, MORE_ACADEMIC, MORE_SCHOLARSHIP, MORE_EVENT, DETAILED,
- SETTINGS, OSS, CS, SEARCH, NOTIFICATION, BOOKMARKS, EDIT_BOOKMARK, Unspecified }
-
-// Navigation Destinations
-@Serializable
-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 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
deleted file mode 100644
index dfe76ae3..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-package com.doyoonkim.knutice.navigation
-
-import android.util.Log
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.toRoute
-import com.doyoonkim.knutice.model.Destination
-import com.doyoonkim.knutice.model.FullContent
-import com.doyoonkim.knutice.model.NavDestination
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.presentation.BookmarkComposable
-import com.doyoonkim.knutice.presentation.CategorizedNotification
-import com.doyoonkim.knutice.presentation.CustomerService
-import com.doyoonkim.knutice.presentation.DetailedNoticeContent
-import com.doyoonkim.knutice.presentation.EditBookmark
-import com.doyoonkim.knutice.presentation.MoreCategorizedNotification
-import com.doyoonkim.knutice.presentation.NotificationPreferences
-import com.doyoonkim.knutice.presentation.OpenSourceLicenseNotice
-import com.doyoonkim.knutice.presentation.UserPreference
-import com.doyoonkim.knutice.presentation.SearchNotice
-import com.doyoonkim.knutice.viewModel.MainServiceViewModel
-
-@Composable
-fun MainNavigator(
- modifier: Modifier = Modifier,
- viewModel: MainServiceViewModel = hiltViewModel(),
- navController: NavHostController,
- onExitAction: (Boolean) -> Unit
-) {
- // Navigation
- NavHost(
- modifier = modifier,
- navController = navController,
- startDestination = NavDestination(arrived = Destination.MAIN),
- ) {
- composable { backStackEntry ->
- val destination = backStackEntry.toRoute()
- viewModel.updateState(
- updatedCurrentLocation = destination.arrived
- )
-
- if (destination.arrived == Destination.CS
- || destination.arrived == Destination.OSS) {
- viewModel.updateState(
- updatedBottomNavBarVisibility = false
- )
- } else {
- viewModel.updateState(
- updatedBottomNavBarVisibility = true
- )
- }
-
- when (destination.arrived) {
- Destination.MAIN -> {
- CategorizedNotification(
- onGoBackAction = { navController.popBackStack().also {
- if (!it) onExitAction(it)
- } },
- onMoreNoticeRequested = { navController.navigate(NavDestination(arrived = it)) },
- onFullContentRequested = {
- viewModel.updateState(updatedTempReservedNoticeForBookmark = it)
- navController.navigate(it.toFullContent())
- }
- )
- }
- Destination.SETTINGS -> UserPreference(
- Modifier.padding(top = 20.dp, start = 10.dp, end = 10.dp).fillMaxSize(),
- onCustomerServiceClicked = { navController.navigate(NavDestination(it))},
- onNotificationPreferenceClicked = { navController.navigate(NavDestination(it)) },
- onOssClicked = { navController.navigate(NavDestination(it)) },
- onBackPressed = { navController.popBackStack().also {
- if (!it) navController.run {
- navController.navigate(NavDestination(Destination.MAIN))
- }
- } }
- )
- Destination.OSS -> OpenSourceLicenseNotice()
- Destination.CS -> { CustomerService(Modifier.padding(15.dp)) }
- Destination.SEARCH -> SearchNotice(
- onBackClicked = { navController.popBackStack().also {
- if (!it) navController.run {
- navController.navigate(NavDestination(Destination.MAIN))
- }
- } },
- onNoticeClicked = {
- viewModel.updateState(updatedTempReservedNoticeForBookmark = it)
- navController.navigate(it.toFullContent())
- }
- )
- Destination.NOTIFICATION -> NotificationPreferences(
- onBackClicked = { navController.popBackStack() },
- onMainNotificationSwitchToggled = { }
- )
- Destination.BOOKMARKS -> BookmarkComposable(
- modifier =Modifier.fillMaxSize(),
- onEachItemClicked = { navController.navigate(it) },
- onBackPressed = { navController.popBackStack().also {
- if (!it) navController.run {
- navController.navigate(NavDestination(Destination.MAIN))
- }
- } }
- )
- else -> MoreCategorizedNotification(
- backButtonHandler = { navController.popBackStack().also {
- if (!it) navController.run {
- navController.navigate(NavDestination(Destination.MAIN))
- }
- } },
- onNoticeSelected = {
- viewModel.updateState(updatedTempReservedNoticeForBookmark = it)
- navController.navigate(it.toFullContent())
- }
- )
- }
- }
-
- composable { backStackEntry ->
- val requestedNotice = backStackEntry.toRoute()
- val scaffoldTitle = requestedNotice.title
-
- 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(
- onBackPressed = { navController.popBackStack().also {
- if (!it) navController.run { navController.navigate(NavDestination(Destination.MAIN)) }
- } }
- )
- Spacer(Modifier.height(20.dp))
- }
-
- composable {
- viewModel.updateState(
- updatedCurrentLocation = Destination.EDIT_BOOKMARK,
- updatedBottomNavBarVisibility = false
- )
- 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) {
- Log.d("MainNavigator", "Bookmark instance received.")
- viewModel.updateState(
- updatedCurrentTargetBookmark = bookmark,
- updatedScheduleTriggered = true
- )
- }
- navController.navigate(NavDestination(Destination.BOOKMARKS))
- }
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt
deleted file mode 100644
index 15760329..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-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
-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.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-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
-
-@Composable
-fun BookmarkComposable(
- modifier: Modifier = Modifier,
- viewModel: BookmarkViewModel = hiltViewModel(),
- onEachItemClicked: (Notice) -> Unit = { },
- onBackPressed: () -> Unit = { }
-) {
- val uiState by viewModel.uiState.collectAsState()
-
- BackHandler {
- onBackPressed()
- }
-
-// LaunchedEffect(uiState.bookmarks) {
-// viewModel.getAllBookmarks()
-// }
-
- Box(
- modifier = modifier.background(MaterialTheme.colorScheme.containerBackgroundSolid)
- .systemBarsPadding()
- ) {
- if (uiState.bookmarks.isEmpty()) {
- Text(
- text = stringResource(R.string.text_no_bookmark),
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.subTitle,
- modifier = Modifier.align(Alignment.Center)
- )
- } else {
- LazyColumn(
- modifier = Modifier.wrapContentHeight()
- .fillMaxWidth()
- .padding(top = 12.dp)
- .background(Color.Transparent),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- items(
- items = uiState.bookmarks,
- key = { it.second.nttId }
- ) {
- // Being called 3 times
- Log.d("BookmarkComposable", "Element: $it")
- NotificationPreviewCardMarked(
- noticeTitle = it.second.title,
- noticeSubtitle = "[${it.second.departName}] ${it.second.timestamp}",
- onItemClicked = { onEachItemClicked(it.second) }
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt
deleted file mode 100644
index cae99d4b..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package com.doyoonkim.knutice.presentation
-
-import android.Manifest
-import android.content.Intent
-import android.os.Bundle
-import android.provider.Settings
-import android.util.Log
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-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
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-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
-
-@AndroidEntryPoint
-class MainActivity : ComponentActivity() {
-
- private val notificationAlarmScheduler by lazy {
- NotificationAlarmScheduler(this)
- }
-
- private val activity = this
-
- 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
- val requestPermissionLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- permissions.entries.forEach {
- Log.d("MainServiceScreen", "${it.key}, ${it.value}")
- if (it.key == Manifest.permission.SCHEDULE_EXACT_ALARM
- && !notificationAlarmScheduler.canScheduleExactAlarms()) {
- showPermissionRationale = true
- }
- }
- }
-
- LaunchedEffect(Unit) {
- // Permission check
- requestPermissionLauncher.launch(
- arrayOf(
- Manifest.permission.POST_NOTIFICATIONS,
- Manifest.permission.SCHEDULE_EXACT_ALARM
- )
- )
-
- // 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)
- ) {
- // onExitAction
- activity.finish()
- }
- }
-
- AnimatedVisibility(
- visible = showPermissionRationale,
- enter = scaleIn(),
- exit = scaleOut()
- ) {
- Box(
- modifier = Modifier.fillMaxSize()
- .clickable { showPermissionRationale = false }
- ) {
- PermissionRationaleComposable(
- modifier = Modifier.align(Alignment.Center).padding(start = 20.dp, end = 20.dp),
- permissionName = stringResource(R.string.title_alarm_and_reminder),
- rationaleTitle = stringResource(R.string.text_rationale_title),
- description = stringResource(R.string.text_rationale_description)
- ) {
- val settingIntent = Intent(
- Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
- ).apply {
- this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- this.putExtra(
- "android.provider.extra.APP_PACKAGE",
- context.packageName
- )
- }
- context.startActivity(settingIntent)
- showPermissionRationale = false
- }
- }
- }
- }
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
-
- viewModelStore.clear()
- this.externalCacheDir?.delete()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt
deleted file mode 100644
index 75444dce..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainServiceScreen.kt
+++ /dev/null
@@ -1,320 +0,0 @@
-package com.doyoonkim.knutice.presentation
-
-import android.util.Log
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-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
-import androidx.compose.material.BottomNavigationItem
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-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
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-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
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun MainServiceScreen(
- viewModel: MainServiceViewModel = hiltViewModel(),
- navController: NavHostController,
- onScheduleAlarmTriggered: (Bookmark) -> Unit, // Should be refactored later
- content: @Composable (PaddingValues) -> Unit
-) {
- val mainAppState by viewModel.uiState.collectAsState()
-
-// //TODO Live translation feature (TBD)
-// var showLanguageDownloadRationale by remember { mutableStateOf(false) }
-// LaunchedEffect(Unit) {
-// if (Locale.getDefault() != Locale.KOREAN) {
-// showLanguageDownloadRationale = true
-// }
-// }
-
- LaunchedEffect(mainAppState.scheduleTriggered) {
- Log.d("MainServiceScreen", "Triggered")
- if (!mainAppState.scheduleTriggered || mainAppState.currentTargetBookmark.bookmarkId == -1) {
- return@LaunchedEffect
- }
- val scheduleResult = async {
- kotlin.runCatching {
- onScheduleAlarmTriggered(mainAppState.currentTargetBookmark)
- }
- }
- scheduleResult.await().fold(
- onSuccess = {
- viewModel.updateState(
- updatedCurrentTargetBookmark = Bookmark(-1),
- updatedScheduleTriggered = false
- )
- },
- onFailure = {
- Log.d("MainServiceScreen", "Unable to schedule alarm.\nREASON:${it}")
- viewModel.updateState(
- updatedScheduleTriggered = false
- )
- }
- )
- }
-
- Scaffold(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.containerBackgroundSolid),
- topBar = {
- TopAppBar(
- title = {
- Row(
- modifier = Modifier.wrapContentSize(),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (mainAppState.currentLocation != Destination.MAIN
- && mainAppState.currentLocation != Destination.BOOKMARKS
- ) {
- IconButton(
- onClick = {
- navController.popBackStack().also {
- viewModel.updateState(updatedFabVisibility = true)
- }
- }
- ) {
- Image(
- painter = if (mainAppState.currentLocation == Destination.CS) {
- painterResource(R.drawable.baseline_close_24)
- } else {
- painterResource(R.drawable.baseline_arrow_back_ios_new_24)
- },
- contentDescription = "back",
- modifier = Modifier.wrapContentSize(),
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
- )
- }
- }
- Text(
- text = when (mainAppState.currentLocation) {
- Destination.MAIN -> stringResource(R.string.app_name)
- Destination.MORE_GENERAL -> stringResource(R.string.general_news)
- Destination.MORE_ACADEMIC -> stringResource(R.string.academic_news)
- Destination.MORE_SCHOLARSHIP -> stringResource(R.string.scholarship_news)
- Destination.MORE_EVENT -> stringResource(R.string.event_news)
- Destination.SETTINGS -> stringResource(R.string.title_preference)
- Destination.OSS -> stringResource(R.string.oss_notice)
- Destination.CS -> stringResource(R.string.title_customer_service)
- Destination.SEARCH -> stringResource(R.string.title_search)
- Destination.NOTIFICATION -> stringResource(R.string.title_notification_pref)
- Destination.BOOKMARKS -> stringResource(R.string.app_name)
- Destination.EDIT_BOOKMARK -> stringResource(R.string.title_edit_bookmark)
- Destination.DETAILED -> mainAppState.currentScaffoldTitle
- Destination.Unspecified -> mainAppState.currentLocation.name
- },
- textAlign = if (mainAppState.currentLocation == Destination.CS ||
- mainAppState.currentLocation == Destination.SEARCH
- ) {
- TextAlign.Center
- } else {
- TextAlign.Start
- },
- fontSize = 20.sp,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.title
- )
- }
- },
- colors = TopAppBarDefaults.topAppBarColors(
- titleContentColor = MaterialTheme.colorScheme.title,
- containerColor = MaterialTheme.colorScheme.containerBackgroundSolid
- ),
- actions = {
- if (mainAppState.currentLocation == Destination.MAIN) {
- IconButton(
- onClick = {
- navController.navigate(NavDestination(Destination.SEARCH))
- }
- ) {
- Image(
- painter = painterResource(R.drawable.baseline_search_24),
- contentDescription = "Search",
- modifier = Modifier.wrapContentSize(),
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
- )
- }
- IconButton(
- onClick = {
- navController.navigate(NavDestination(Destination.SETTINGS))
- }
- ) {
- Image(
- painter = painterResource(R.drawable.baseline_settings_24),
- contentDescription = "Settings",
- modifier = Modifier.wrapContentSize(),
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title)
- )
- }
- }
- }
- )
- },
- floatingActionButton = {
- 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(
- imageVector = Icons.Filled.Add,
- contentDescription = "Floating Action Button",
- tint = Color.White
- )
- }
- }
- },
- bottomBar = {
- AnimatedVisibility(
- visible = mainAppState.currentLocation == Destination.MAIN
- || mainAppState.currentLocation == Destination.BOOKMARKS,
- enter = slideInVertically(initialOffsetY = { it + (it * 1 / 2) }),
- exit = slideOutVertically(targetOffsetY = { it + (it * 1 / 2) })
- ) {
- BottomAppBar(
- modifier = Modifier
- .background(Color.Transparent),
-// .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
-// .offset(y = 20.dp)
- actions = {
- BottomNavigationItem(
- selected = mainAppState.currentLocation == Destination.MAIN,
- enabled = true,
- onClick = {
- navController.navigate(NavDestination(Destination.MAIN))
- },
- icon = {
- Icon(
- painter = painterResource(R.drawable.baseline_home_24),
- contentDescription = "Main",
- modifier = Modifier.padding(bottom = 5.dp)
- )
- },
- label = {
- Text(stringResource(R.string.bottom_bar_home))
- },
- selectedContentColor = MaterialTheme.colorScheme.title,
- unselectedContentColor = MaterialTheme.colorScheme.subTitle
- )
- BottomNavigationItem(
- selected = mainAppState.currentLocation == Destination.BOOKMARKS,
- enabled = true,
- onClick = {
- navController.navigate(NavDestination(Destination.BOOKMARKS))
- },
- icon = {
- Icon(
- painter = painterResource(R.drawable.baseline_bookmarks_24),
- contentDescription = "Bookmarks",
- modifier = Modifier.padding(bottom = 5.dp)
- )
- },
- label = {
- Text(stringResource(R.string.bottom_bar_bookmark))
- },
- selectedContentColor = MaterialTheme.colorScheme.title,
- unselectedContentColor = MaterialTheme.colorScheme.subTitle
- )
- },
- containerColor = MaterialTheme.colorScheme.bottomNavContainer,
- contentColor = MaterialTheme.colorScheme.title
- )
- }
- if (mainAppState.currentLocation != Destination.EDIT_BOOKMARK) {
-
- }
- },
- containerColor = Color.Transparent
- ) { innerPadding ->
-
- content(innerPadding)
-
- //TODO Live Translation Feature (TBD)
-// AnimatedVisibility(
-// visible = showLanguageDownloadRationale,
-// enter = scaleIn(),
-// exit = scaleOut()
-// ) {
-// Box(
-// modifier = Modifier.fillMaxSize()
-// .clickable { showLanguageDownloadRationale = false }
-// ) {
-// PermissionRationaleComposable(
-// modifier = Modifier.align(Alignment.Center).padding(start = 20.dp, end = 20.dp),
-// permissionName = stringResource(R.string.text_language),
-// rationaleTitle = stringResource(R.string.title_langauge_model_download),
-// description = stringResource(R.string.description_language_model_download)
-// ) {
-// viewModel.requestModelDownload()
-// showLanguageDownloadRationale = true
-// }
-// }
-// }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt
deleted file mode 100644
index 921020cf..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/NotificationPerferences.kt
+++ /dev/null
@@ -1,239 +0,0 @@
-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
-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.material3.HorizontalDivider
-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
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-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
-import com.doyoonkim.knutice.ui.theme.subTitle
-import com.doyoonkim.knutice.ui.theme.title
-import com.doyoonkim.knutice.viewModel.NotificationPreferenceViewModel
-
-@Composable
-fun NotificationPreferences(
- modifier: Modifier = Modifier,
- viewModel: NotificationPreferenceViewModel = hiltViewModel(),
- onBackClicked: () -> Unit = { },
- onMainNotificationSwitchToggled: () -> Unit = { }
-) {
- val context = LocalContext.current
- val status by viewModel.uiStatus.collectAsState()
- var permissionStatus by remember { mutableStateOf(false) }
-
- LaunchedEffect(Unit) {
- viewModel.checkMainNotificationPreferenceStatus()
- viewModel.checkTopicSubscriptionStatus()
- }
-
- LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
- viewModel.checkMainNotificationPreferenceStatus()
- }
-
- Column(
- modifier = Modifier.fillMaxSize()
- .systemBarsPadding()
- .padding(10.dp),
- verticalArrangement = Arrangement.Top,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Column(
- modifier = modifier,
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Top
- ) {
- 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.fillMaxWidth().wrapContentSize()
- .padding(top = 15.dp, bottom = 15.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- 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 = status.isMainNotificationPermissionGranted,
- 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),
- isSwitchChecked = status.isEachChannelAllowed[0],
- isSwitchEnabled = status.isMainNotificationPermissionGranted
- ) {
- viewModel.updateChannelPreference(NoticeCategory.GENERAL_NEWS, it)
- }
-
- LabeledToggleSwitch(
- modifier = Modifier.padding(start = 10.dp),
- titleText = stringResource(R.string.academic_notification_channel_name),
- subTitleText = stringResource(R.string.academic_notification_channel_description),
- isSwitchChecked = status.isEachChannelAllowed[1],
- isSwitchEnabled = status.isMainNotificationPermissionGranted
- ) {
- viewModel.updateChannelPreference(NoticeCategory.ACADEMIC_NEWS, it)
- }
- LabeledToggleSwitch(
- modifier = Modifier.padding(start = 10.dp),
- titleText = stringResource(R.string.scholarship_notification_channel_name),
- subTitleText = stringResource(R.string.scholarship_notification_channel_description),
- isSwitchChecked = status.isEachChannelAllowed[2],
- isSwitchEnabled = status.isMainNotificationPermissionGranted
- ) {
- viewModel.updateChannelPreference(NoticeCategory.SCHOLARSHIP_NEWS, it)
- }
- LabeledToggleSwitch(
- modifier = Modifier.padding(start = 10.dp),
- titleText = stringResource(R.string.event_notification_channel_name),
- subTitleText = stringResource(R.string.event_notification_channel_description),
- isSwitchChecked = status.isEachChannelAllowed[3],
- isSwitchEnabled = status.isMainNotificationPermissionGranted
- ) {
- viewModel.updateChannelPreference(NoticeCategory.EVENT_NEWS, it)
- }
- }
- }
-}
-
-@Composable
-fun LabeledToggleSwitch(
- modifier: Modifier = Modifier,
- titleText: String = "Title Text",
- subTitleText: String = "Subtitle Text",
- isSwitchChecked: Boolean = false,
- isSwitchEnabled: Boolean = false,
- onCheckStatusChanged: (Boolean) -> Unit = { }
-) {
- Column(
- modifier = modifier.fillMaxWidth().wrapContentSize()
- .padding(top = 15.dp, bottom = 15.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(
- modifier = Modifier.wrapContentHeight().weight(5f),
- verticalArrangement = Arrangement.spacedBy(5.dp)
- ) {
- Text(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
- text = titleText,
- color = MaterialTheme.colorScheme.title,
- fontWeight = FontWeight.Medium,
- fontSize = 18.sp,
- textAlign = TextAlign.Start
- )
-
- Text(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
- text = subTitleText,
- color = MaterialTheme.colorScheme.subTitle,
- fontWeight = FontWeight.Normal,
- fontSize = 12.sp,
- textAlign = TextAlign.Start
- )
- }
-
- Switch(
- checked = isSwitchChecked,
- onCheckedChange = {
- onCheckStatusChanged(it)
- },
- enabled = isSwitchEnabled
- )
- }
- }
-}
-
-@Preview(locale = "ko-rKR")
-@Composable
-fun NotificationPreferences_Preview() {
- NotificationPreferences()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/OssNotice.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/OssNotice.kt
deleted file mode 100644
index 21c27196..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/OssNotice.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.doyoonkim.knutice.presentation
-
-import android.webkit.WebView
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-
-@Composable
-fun OpenSourceLicenseNotice(
- modifier: Modifier = Modifier
-) {
- AndroidView(
- modifier = modifier.padding(),
- factory = { context ->
- WebView(context).apply {
- loadUrl("https://knutice.github.io/KNUTICE-OpenSourceLicense/Android/opensource.html")
- }
- }
- )
-}
-
-@Composable
-@Preview
-fun OpenSourceLicenseNotice_Preview() {
- OpenSourceLicenseNotice()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt
deleted file mode 100644
index 26ec5919..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.domain.FetchBookmarkFromDatabase
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.BookmarkComposableState
-import com.doyoonkim.knutice.model.Notice
-import dagger.hilt.android.lifecycle.HiltViewModel
-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.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class BookmarkViewModel @Inject constructor(
- private val localRepository: NoticeLocalRepository,
- private val fetchBookmarkFromDatabase: FetchBookmarkFromDatabase
-): ViewModel() {
- // Prevent direct access to the UI STATE from the 'View'
- private var _uiState = MutableStateFlow(BookmarkComposableState())
- val uiState = _uiState.asStateFlow()
-
- init {
- getAllBookmarks()
- }
-
- fun updateBookmarks(newPair: Pair) {
- _uiState.update {
- it.copy(
- bookmarks = uiState.value.bookmarks.toMutableList().apply {
- this.add(newPair)
- }.distinctBy { e -> e.first.bookmarkId }.toList()
-// bookmarks = newList
- )
- }
-
- Log.d("BookmarkViewModel", "Updated Bookmark Status: ${uiState.value.bookmarks}")
- }
-
- fun getAllBookmarks() {
- viewModelScope.launch(Dispatchers.IO) {
- fetchBookmarkFromDatabase.getAllBookmarkWithNotices()
- .map { Result.success(it) }
- .catch { emit(Result.failure(it)) }
- .collectLatest { result ->
- result.fold(
- onSuccess = {
- Log.d("BookmarkViewModel", "Collected: ${it.toString()}")
- updateBookmarks(it)
- },
- onFailure = {
- Log.d("BookmarkViewModel", "Unable to receive bookmark pair.\nREASON:${it.message}")
- }
- )
- }
- }
- }
-
-}
-
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt
deleted file mode 100644
index e8c7367f..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.doyoonkim.knutice.domain.FetchTopThreeNoticeByCategory
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class CategorizedNotificationViewModel @Inject constructor(
- private val fetchTopThreeNoticeUseCase: FetchTopThreeNoticeByCategory
-) : ViewModel() {
- init {
- viewModelScope.launch(Dispatchers.Default) {
- fetchTopThreeNoticesPerCategory(NoticeCategory.GENERAL_NEWS)
- fetchTopThreeNoticesPerCategory(NoticeCategory.ACADEMIC_NEWS)
- fetchTopThreeNoticesPerCategory(NoticeCategory.SCHOLARSHIP_NEWS)
- fetchTopThreeNoticesPerCategory(NoticeCategory.EVENT_NEWS)
- }
- }
-
- private val fileName = "CategorizedNotificationViewModel"
- private val _uiState = MutableStateFlow(CategorizedNotificationState())
- var uiState: StateFlow = _uiState.asStateFlow()
-
- fun updateState (
- updatedNotificationGeneral: List = _uiState.value.notificationGeneral,
- updatedNotificationAcademic: List = _uiState.value.notificationAcademic,
- updatedNotificationScholarship: List = _uiState.value.notificationScholarship,
- updatedNotificationEvent: List = _uiState.value.notificationEvent
- ) {
- viewModelScope.launch(Dispatchers.Default) {
- _uiState.update {
- it.copy(
- notificationGeneral = updatedNotificationGeneral,
- notificationAcademic = updatedNotificationAcademic,
- notificationScholarship = updatedNotificationScholarship,
- notificationEvent = updatedNotificationEvent
- )
- }
- }
- }
-
- private suspend fun fetchTopThreeNoticesPerCategory(category: NoticeCategory) {
- fetchTopThreeNoticeUseCase.getTopThreeNotices(category)
- .map { Result.success(it) }
- .catch { emit(Result.failure(it)) }
- .collectLatest { result ->
- result.fold(
- onSuccess = {
- val notices = listOf(it.notice1!!, it.notice2!!, it.notice3!!)
- when(category) {
- NoticeCategory.GENERAL_NEWS -> {
-
- updateState(
- updatedNotificationGeneral = notices
- )
- }
- NoticeCategory.ACADEMIC_NEWS -> updateState(
- updatedNotificationAcademic = notices
- )
- NoticeCategory.SCHOLARSHIP_NEWS -> updateState(
- updatedNotificationScholarship = notices
- )
- NoticeCategory.EVENT_NEWS -> updateState(
- updatedNotificationEvent = notices
- )
- else -> { }
- }
- },
- onFailure = {
- Log.d(fileName, "Retrofit2: Failure: ${it.toString()}")
- }
- )
- }
- }
-
-}
-
-data class CategorizedNotificationState(
- val notificationGeneral: List = listOf(Notice(), Notice(), Notice()),
- val notificationAcademic: List = listOf(Notice(), Notice(), Notice()),
- val notificationScholarship: List = listOf(Notice(), Notice(), Notice()),
- val notificationEvent: List = listOf(Notice(), Notice(), Notice())
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt
deleted file mode 100644
index 663acc97..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.content.Context
-import android.os.Build
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.doyoonkim.knutice.R
-import com.doyoonkim.knutice.data.KnuticeRemoteSource
-import com.doyoonkim.knutice.model.CustomerServiceReportState
-import com.doyoonkim.knutice.model.ReportRequest
-import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
-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 kotlinx.coroutines.withContext
-import javax.inject.Inject
-
-@HiltViewModel
-class CustomerServiceViewModel @Inject constructor(
- @ApplicationContext private val context: Context,
- private val remoteSource: KnuticeRemoteSource
-) : ViewModel() {
-
- private var _uiState = MutableStateFlow(CustomerServiceReportState())
- val uiState = _uiState.asStateFlow()
-
- fun updateUserReportContent(content: String) {
- if (!_uiState.value.reachedMaxCharacters) {
- viewModelScope.launch(Dispatchers.Default) {
- _uiState.update {
- it.copy(
- userReport = content,
- exceedMinCharacters = content.length >= 5,
- reachedMaxCharacters = content.length >= 500
- )
- }
- }
- }
- }
-
- fun updateCompletionState() {
- _uiState.update {
- it.copy(
- isSubmissionCompleted = false,
- isSubmissionFailed = false
- )
- }
- }
-
- fun submitUserReport() {
- val report = ReportRequest(
- content = _uiState.value.userReport,
- deviceName = "${Build.BRAND} ${Build.MODEL}",
- version = context.getString(R.string.version_code)
- )
-
- Log.d("CustomerServiceViewModel", "Generated Report:\n\t${report.toString()}")
-
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- val submission = async { remoteSource.submitUserReport(report) }
-
- submission.await().fold(
- onSuccess = { submissionResult ->
- if (submissionResult) {
- _uiState.update {
- it.copy(
- userReport = "",
- reachedMaxCharacters = false,
- isSubmissionFailed = false,
- isSubmissionCompleted = true
- )
- }
- } else {
- _uiState.update {
- it.copy(
- isSubmissionFailed = true,
- isSubmissionCompleted = true
- )
- }
- }
- },
- onFailure = {
- _uiState.update {
- it.copy(
- isSubmissionFailed = true,
- isSubmissionCompleted = true
- )
- }
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt
deleted file mode 100644
index 0af7ea17..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/DetailedNoticeContentViewModel.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-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
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class DetailedNoticeContentViewModel @Inject constructor(
- private val fetchSingleNoticeImpl: FetchSingleNoticeImpl,
- private val savedStateHandle: SavedStateHandle
-) : ViewModel() {
-
- private var _uiState = MutableStateFlow(DetailedContentState())
- val uiState = _uiState.asStateFlow()
-
- private val requested = savedStateHandle.toRoute()
-
- init {
- _uiState.update {
- it.copy(
- url = requested.url
- )
- }
- requestNoticeById(requested.nttId!!)
- }
-
- fun updateLoadingStatus(newStatus: Int) {
- Log.d("DetailedNoticeContentViewModel", "Update loading status")
- viewModelScope.launch {
- _uiState.update {
- it.copy(
- loadingStatue = (newStatus / 100).toFloat()
- )
- }
- }
- }
-
- 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/EditBookmarkViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/EditBookmarkViewModel.kt
deleted file mode 100644
index 1fff3b75..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/EditBookmarkViewModel.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.util.Log
-import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import androidx.navigation.toRoute
-import com.doyoonkim.knutice.data.NoticeLocalRepository
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.EditBookmarkState
-import com.doyoonkim.knutice.model.Notice
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class EditBookmarkViewModel @Inject constructor(
- private val localRepository: NoticeLocalRepository,
- private val savedStateHandle: SavedStateHandle
-) : ViewModel() {
-
- private var _uiState = MutableStateFlow(EditBookmarkState())
- val uiState: StateFlow = _uiState.asStateFlow()
-
- private val requestNotice = savedStateHandle.toRoute()
-
- init {
- _uiState.update {
- it.copy(
- targetNotice = requestNotice
- )
- }
- getBookmarkByNotice()
- }
-
- fun updateReminderOptions(
- reminderRequested: Boolean = uiState.value.isReminderRequested,
- updatedTimeForRemind: Long = uiState.value.timeForRemind,
- updatedDatePickerVisible: Boolean = uiState.value.datePickerVisible
- ) {
- _uiState.update {
- it.copy(
- isReminderRequested = reminderRequested,
- timeForRemind = updatedTimeForRemind,
- datePickerVisible = updatedDatePickerVisible
- )
- }
- }
-
- fun updateBookmarkNotes(newString: String) {
- viewModelScope.launch(Dispatchers.Default) {
- if (uiState.value.bookmarkNote.length < 500) {
- _uiState.update {
- it.copy(
- bookmarkNote = newString
- )
- }
- }
- }
- }
-
- private fun updateBookmarkInstance() {
- val updatedInstance = Bookmark(
- uiState.value.bookmarkId,
- uiState.value.isReminderRequested,
- uiState.value.timeForRemind,
- uiState.value.bookmarkNote,
- uiState.value.targetNotice.nttId
- )
- _uiState.update {
- it.copy(
- bookmarkInstance = updatedInstance
- )
- }
- }
-
- private fun getBookmarkByNotice() {
- viewModelScope.launch(Dispatchers.IO) {
- localRepository.getBookmarkByNttID(requestNotice.nttId)
- .fold(
- onSuccess = { bookmark ->
- if (bookmark.bookmarkId != -1) {
- _uiState.update {
- it.copy(
- bookmarkId = bookmark.bookmarkId ?: 0,
- isReminderRequested = bookmark.isScheduled ?: false,
- timeForRemind = bookmark.reminderSchedule ?: 0,
- bookmarkNote = bookmark.note ?: "",
- requireCreation = false
- )
- }
- }
- },
- onFailure = {
- Log.d("EditBookmarkViewModel", "There is no existing bookmark for this notice.")
- }
- )
- }
- }
-
- fun createNewBookmark() {
- viewModelScope.launch(Dispatchers.IO) {
- val targetNotice = uiState.value.targetNotice
- val newBookmark = Bookmark(
- bookmarkId = 0,
- isScheduled = uiState.value.isReminderRequested,
- reminderSchedule = 0,
- note = uiState.value.bookmarkNote,
- nttId = targetNotice.nttId
- )
-
- localRepository.createBookmark(newBookmark, targetNotice).fold(
- onSuccess = {
- Log.d("EditBookmarkViewModel", "Creation Inquiry Processed Successfully with state: $it")
- updateBookmarkInstance()
- },
- onFailure = {
- Log.d("EditBookmarkViewModel", "Unable to process creation inquiry\nREASON:${it.message}")
- }
- )
- }
- }
-
- fun modifyBookmark() {
- viewModelScope.launch(Dispatchers.IO) {
- val targetNotice = uiState.value.targetNotice
- val modifiedBookmark = Bookmark(
- bookmarkId = uiState.value.bookmarkId,
- isScheduled = uiState.value.isReminderRequested,
- reminderSchedule = uiState.value.timeForRemind,
- note = uiState.value.bookmarkNote,
- nttId = uiState.value.targetNotice.nttId
- )
-
- kotlin.runCatching {
- localRepository.updateBookmark(modifiedBookmark)
- updateBookmarkInstance()
- }.onFailure { Log.d("EditBookmarkViewModel", "Unable to modify bookmark") }
-
- }
- }
-
- fun removeBookmark() {
- viewModelScope.launch(Dispatchers.IO) {
- val targetBookmark = Bookmark(
- bookmarkId = uiState.value.bookmarkId,
- isScheduled = uiState.value.isReminderRequested,
- reminderSchedule = uiState.value.timeForRemind,
- note = uiState.value.bookmarkNote,
- nttId = uiState.value.targetNotice.nttId
- )
-
- // The code snippet below should be isolated into separated class under the domain layer.
- // Remove related notice entity first.
- localRepository.deleteNoticeEntity(uiState.value.targetNotice.toNoticeEntity())
- // Once notice entity is being deleted, target bookmark would be deleted.
- localRepository.deleteBookmark(targetBookmark)
- }
- }
-
-}
\ 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
deleted file mode 100644
index 444cf3a7..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/MainServiceViewModel.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-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(
- 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
- ) {
- _uiState.update {
- it.copy(
- currentLocation = updatedCurrentLocation,
- currentScaffoldTitle = updatedCurrentScaffoldTitle,
- isBottomNavBarVisible = updatedBottomNavBarVisibility,
- isFabVisible = updatedFabVisibility,
- tempReserveNoticeForBookmark = updatedTempReservedNoticeForBookmark,
- currentTargetBookmark = updatedCurrentTargetBookmark,
- scheduleTriggered = updatedScheduleTriggered
- )
- }
- }
-
- fun updateLanguageModelDownloadStatus(newStatus: String) {
- _uiState.update {
- it.copy(
- languageModelDownloadResult = newStatus
- )
- }
- }
-
- 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 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"
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt
deleted file mode 100644
index 3d110589..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.util.Log
-import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.ViewModel
-import androidx.navigation.toRoute
-import com.doyoonkim.knutice.domain.FetchNoticesPerPageInCategory
-import com.doyoonkim.knutice.model.Destination
-import com.doyoonkim.knutice.model.NavDestination
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.model.NoticeCategory
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineScope
-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.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class MoreCategorizedNotificationViewModel @Inject constructor(
- private val fetchListOfNoticesUseCase: FetchNoticesPerPageInCategory,
- private val savedStateHandle: SavedStateHandle
-): ViewModel() {
- private val filename = "MoreCategorizedNotificationViewModel"
-
- // UI State
- private var _uiState = MutableStateFlow(MoreNotificationListState())
- val uiState = _uiState.asStateFlow()
-
- // Category of Requested Notice List
- private val category = savedStateHandle.toRoute().run {
- when (this.arrived) {
- Destination.MORE_GENERAL -> NoticeCategory.GENERAL_NEWS
- Destination.MORE_ACADEMIC -> NoticeCategory.ACADEMIC_NEWS
- Destination.MORE_SCHOLARSHIP -> NoticeCategory.SCHOLARSHIP_NEWS
- Destination.MORE_EVENT -> NoticeCategory.EVENT_NEWS
- else -> NoticeCategory.Unspecified
- }
- }
-
- fun requestRefresh() {
- _uiState.update {
- it.copy(
- currentLastNttId = 0,
- notices = List(20) { Notice() },
- isRefreshRequested = true
- )
- }
- fetchNotificationPerPage()
- }
-
- fun requestMoreNotices() {
- if (!_uiState.value.isLoading) {
- fetchNotificationPerPage()
- }
- }
-
- fun fetchNotificationPerPage() {
- CoroutineScope(Dispatchers.IO).launch {
- fetchListOfNoticesUseCase.getNoticesPerPage(
- category, _uiState.value.currentLastNttId
- )
- .map { Result.success(it) }
- .catch { emit(Result.failure(it)) }
- .collectLatest { result ->
- result.fold(
- onSuccess = { received ->
- _uiState.update {
- it.copy(
- currentLastNttId = received.last().nttId,
- notices = if (_uiState.value.currentLastNttId == 0) {
- received
- } else {
- it.notices.addAll(received)
- },
- isLoading = false,
- isRefreshRequested = false
- )
- }
- },
- onFailure = { exception ->
- Log.d(filename, "Unable to received data.\nReason: ${exception.message}")
- _uiState.update {
- it.copy(isLoading = false, isRefreshRequested = false)
- }
- }
- )
- }
- }
- }
-
- private fun List.addAll(additionalElements: List): List {
- return List(this.size + additionalElements.size) {
- if (it in indices) this[it]
- else additionalElements[it - this.size]
- }
- }
-
-}
-
-data class MoreNotificationListState(
- val currentLastNttId: Int = 0,
- val notices: List = List(20) { Notice() },
- val isLoading: Boolean = false,
- val isRefreshRequested: Boolean = false
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt
deleted file mode 100644
index 113a4024..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.Manifest
-import android.app.NotificationManager
-import android.content.Context
-import android.content.Context.NOTIFICATION_SERVICE
-import android.content.pm.PackageManager
-import android.util.Log
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.doyoonkim.knutice.R
-import com.doyoonkim.knutice.data.KnuticeRemoteSource
-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.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class NotificationPreferenceViewModel @Inject constructor(
- @ApplicationContext private val context: Context,
- private val remoteSource: KnuticeRemoteSource
-) : ViewModel() {
-
- private var _uiStatus = MutableStateFlow(NotificationPreferenceStatus())
- val uiStatus = _uiStatus.asStateFlow()
-
- private val notificationChannels = hashMapOf(
- NoticeCategory.GENERAL_NEWS to 0,
- NoticeCategory.ACADEMIC_NEWS to 1,
- NoticeCategory.SCHOLARSHIP_NEWS to 2,
- NoticeCategory.EVENT_NEWS to 3
- )
-
-
- // Needed to be refined later. (If user enters this point without initial permission allowance.
- fun checkMainNotificationPreferenceStatus() {
- val isNotificationAllowed = ContextCompat.checkSelfPermission(
- context, Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED
-
- val isMainChannelAllowed = (context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
- .getNotificationChannel(context.getString(R.string.inapp_notification_channel_id))
- .importance > 0
-
- if (isNotificationAllowed) {
- if (!isMainChannelAllowed) updateMainNotificationStatus(false)
- else updateMainNotificationStatus(true)
- } else {
- updateMainNotificationStatus(false)
- }
- }
-
- fun updateMainNotificationStatus(status: Boolean) {
- _uiStatus.update {
- it.copy(
- isMainNotificationPermissionGranted = status
- )
- }
- }
-
- 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
- )
- }
- }
- )
- }
- }
-
- fun updateChannelPreference(id: NoticeCategory, status: Boolean) {
- val updatedStatus = List(_uiStatus.value.isEachChannelAllowed.size) {
- if (it == notificationChannels[id]) status
- else _uiStatus.value.isEachChannelAllowed[it]
- }
-
- _uiStatus.update {
- it.copy(
- isEachChannelAllowed = updatedStatus
- )
- }
-
- // 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/java/com/doyoonkim/knutice/viewModel/SearchNoticeViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/SearchNoticeViewModel.kt
deleted file mode 100644
index 65b43ac1..00000000
--- a/app/src/main/java/com/doyoonkim/knutice/viewModel/SearchNoticeViewModel.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.doyoonkim.knutice.viewModel
-
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.doyoonkim.knutice.domain.QueryNoticesUsingKeyword
-import com.doyoonkim.knutice.model.SearchNoticeState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class SearchNoticeViewModel @Inject constructor(
- private val queryNoticesUsingKeywordUseCase: QueryNoticesUsingKeyword
-) : ViewModel() {
-
- // State
- private var _uiState = MutableStateFlow(SearchNoticeState())
- val uiState = _uiState.asStateFlow()
-
- fun updateKeyword(newKeyword: String) {
- _uiState.update {
- it.copy(
- searchKeyword = newKeyword
- )
- }
- }
-
- fun updateQueryStatue(newStatue: Boolean) = _uiState.update { it.copy(isQuerying = newStatue) }
-
- fun queryNoticeByKeyword(newKeyword: String) {
- Log.d("SearchNoticeViewModel", "Query keyword ($newKeyword ) would be initiated.")
- viewModelScope.launch {
- updateQueryStatue(true)
- queryNoticesUsingKeywordUseCase.getNoticesByKeyword(newKeyword)
- .map { Result.success(it) }
- .catch { emit(Result.failure(it)) }
- .collectLatest { result ->
- result.fold(
- onSuccess = { list ->
- _uiState.update {
- it.copy(
- queryResult = list,
- isQuerying = false
- )
- }
- },
- onFailure = {
- updateQueryStatue(false)
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 404d250a..3c485967 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,10 +4,15 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
// Dagger Hilt for Dependency Injection
- id("com.google.dagger.hilt.android") version "2.51.1" apply false
+ id("com.google.dagger.hilt.android") version "2.56.2" apply false
alias(libs.plugins.google.gms.google.services) apply false
alias(libs.plugins.kotlinSerialization) apply false
+ alias(libs.plugins.android.library) apply false
+
+ // Required from Kotlin 2.0.0
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.jetbrains.kotlin.jvm) apply false
// KSP Plugin for Room Database
// id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
diff --git a/common/.gitignore b/common/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
new file mode 100644
index 00000000..12cf86d8
--- /dev/null
+++ b/common/build.gradle.kts
@@ -0,0 +1,75 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ id("kotlin-kapt")
+
+ // Required from Kotlin 2.0.0 (Every module using Compose)
+ alias(libs.plugins.compose.compiler)
+
+ alias(libs.plugins.kotlinSerialization)
+}
+
+android {
+ namespace = "com.doyoonkim.common"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 30
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(platform(libs.androidx.compose.bom))
+
+
+ testImplementation(libs.junit)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
+ // Coil
+ implementation(libs.coil.compose)
+
+ // Navigation For Compose
+ implementation(libs.androidx.navigation.compose)
+
+ implementation(libs.kotlin.serialization)
+}
\ No newline at end of file
diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/common/src/androidTest/java/com/doyoonkim/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/com/doyoonkim/common/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..737f5b79
--- /dev/null
+++ b/common/src/androidTest/java/com/doyoonkim/common/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.common
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.common.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/common/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/java/com/doyoonkim/common/BitmapHandler.kt b/common/src/main/java/com/doyoonkim/common/BitmapHandler.kt
new file mode 100644
index 00000000..fad3d092
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/BitmapHandler.kt
@@ -0,0 +1,18 @@
+package com.doyoonkim.common
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import javax.inject.Inject
+
+/**
+ * @author kimdoyoon
+ * Created 6/16/25 at 11:53 PM
+ */
+interface BitmapHandler {
+ fun decodeByteArray(byteArray: ByteArray): Bitmap
+}
+
+class BitmapHandlerImpl @Inject constructor() : BitmapHandler {
+ override fun decodeByteArray(byteArray: ByteArray): Bitmap =
+ BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
+}
diff --git a/common/src/main/java/com/doyoonkim/common/di/AppInjectorProvider.kt b/common/src/main/java/com/doyoonkim/common/di/AppInjectorProvider.kt
new file mode 100644
index 00000000..4376e020
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/di/AppInjectorProvider.kt
@@ -0,0 +1,12 @@
+package com.doyoonkim.common.di
+
+/**
+ * Use it with extreme cautions
+ */
+interface AppInjectorProvider {
+ val appInjector: AppInjector
+}
+
+interface AppInjector {
+ fun inject(target: Any)
+}
\ No newline at end of file
diff --git a/common/src/main/java/com/doyoonkim/common/di/ApplicationContext.kt b/common/src/main/java/com/doyoonkim/common/di/ApplicationContext.kt
new file mode 100644
index 00000000..5828207a
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/di/ApplicationContext.kt
@@ -0,0 +1,7 @@
+package com.doyoonkim.common.di
+
+import javax.inject.Qualifier
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApplicationContext
diff --git a/common/src/main/java/com/doyoonkim/common/di/CommonModule.kt b/common/src/main/java/com/doyoonkim/common/di/CommonModule.kt
new file mode 100644
index 00000000..4e9f558a
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/di/CommonModule.kt
@@ -0,0 +1,20 @@
+package com.doyoonkim.common.di
+
+import com.doyoonkim.common.BitmapHandler
+import com.doyoonkim.common.BitmapHandlerImpl
+import dagger.Module
+import dagger.Binds
+
+/**
+ * @author kimdoyoon
+ * Created 6/17/25 at 12:14 AM
+ */
+@Module
+abstract class CommonModule {
+
+ @Binds
+ abstract fun bindsBitmapHandler(
+ impl: BitmapHandlerImpl
+ ): BitmapHandler
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/com/doyoonkim/common/di/ViewModelKey.kt b/common/src/main/java/com/doyoonkim/common/di/ViewModelKey.kt
new file mode 100644
index 00000000..87f61f6a
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/di/ViewModelKey.kt
@@ -0,0 +1,11 @@
+package com.doyoonkim.common.di
+
+import androidx.lifecycle.ViewModel
+import dagger.MapKey
+import kotlin.reflect.KClass
+
+@MustBeDocumented
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@MapKey
+annotation class ViewModelKey(val value: KClass)
diff --git a/common/src/main/java/com/doyoonkim/common/navigation/BookmarkInfo.kt b/common/src/main/java/com/doyoonkim/common/navigation/BookmarkInfo.kt
new file mode 100644
index 00000000..adeefd3b
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/navigation/BookmarkInfo.kt
@@ -0,0 +1,10 @@
+package com.doyoonkim.common.navigation
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class BookmarkInfo(
+ val noticeId: Int,
+ val noticeTitle: String,
+ val noticeInfo: String
+)
diff --git a/common/src/main/java/com/doyoonkim/common/navigation/NavRoutes.kt b/common/src/main/java/com/doyoonkim/common/navigation/NavRoutes.kt
new file mode 100644
index 00000000..05b443f4
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/navigation/NavRoutes.kt
@@ -0,0 +1,23 @@
+package com.doyoonkim.common.navigation
+
+
+sealed class NavRoutes(val route: String) {
+
+ data object Home : NavRoutes(Destination.HOME.name)
+ data object Bookmark: NavRoutes(Destination.BOOKMARKS.name)
+
+ data object Settings: NavRoutes(Destination.SETTINGS.name)
+ data object NotificationPreferences: NavRoutes(Destination.NOTIFICATION.name)
+ data object CustomerService: NavRoutes(Destination.CS.name)
+ data object OpenSource: NavRoutes(Destination.OSS.name)
+
+ data object GeneralNotices: NavRoutes(Destination.MORE_GENERAL.name)
+ data object AcademicNotices: NavRoutes(Destination.MORE_ACADEMIC.name)
+ data object ScholarshipNotices: NavRoutes(Destination.MORE_SCHOLARSHIP.name)
+ data object EventNotices: NavRoutes(Destination.MORE_EVENT.name)
+
+ data object NoticeSearch: NavRoutes(Destination.SEARCH.name)
+}
+
+enum class Destination { HOME, MORE_GENERAL, MORE_ACADEMIC, MORE_SCHOLARSHIP, MORE_EVENT, DETAILED,
+ SETTINGS, OSS, CS, SEARCH, NOTIFICATION, BOOKMARKS, EDIT_BOOKMARK, Unspecified }
\ No newline at end of file
diff --git a/common/src/main/java/com/doyoonkim/common/navigation/NoticeDetail.kt b/common/src/main/java/com/doyoonkim/common/navigation/NoticeDetail.kt
new file mode 100644
index 00000000..18c264ae
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/navigation/NoticeDetail.kt
@@ -0,0 +1,10 @@
+package com.doyoonkim.common.navigation
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class NoticeDetail(
+ val nttId: Int,
+ val contentUrl: String,
+ val isFabVisible: Boolean = true
+)
diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt b/common/src/main/java/com/doyoonkim/common/theme/Color.kt
similarity index 96%
rename from app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt
rename to common/src/main/java/com/doyoonkim/common/theme/Color.kt
index 3f67b694..cd9a5ec2 100644
--- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt
+++ b/common/src/main/java/com/doyoonkim/common/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.ui.theme
+package com.doyoonkim.common.theme
import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt b/common/src/main/java/com/doyoonkim/common/theme/Theme.kt
similarity index 97%
rename from app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt
rename to common/src/main/java/com/doyoonkim/common/theme/Theme.kt
index 01d391f9..a3a58736 100644
--- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt
+++ b/common/src/main/java/com/doyoonkim/common/theme/Theme.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.ui.theme
+package com.doyoonkim.common.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
@@ -79,6 +79,10 @@ val ColorScheme.buttonContainer: Color
@Composable
get() = if(isSystemInDarkTheme()) ButtonDark else ButtonLight
+val ColorScheme.buttonPurple: Color
+ @Composable
+ get() = Purple40
+
val ColorScheme.textPurple: Color
@Composable
get() = if(isSystemInDarkTheme()) Purple80 else Purple40
diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Type.kt b/common/src/main/java/com/doyoonkim/common/theme/Type.kt
similarity index 96%
rename from app/src/main/java/com/doyoonkim/knutice/ui/theme/Type.kt
rename to common/src/main/java/com/doyoonkim/common/theme/Type.kt
index 4a0a7bef..3031a5bb 100644
--- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Type.kt
+++ b/common/src/main/java/com/doyoonkim/common/theme/Type.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.ui.theme
+package com.doyoonkim.common.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/AnimatedGradient.kt b/common/src/main/java/com/doyoonkim/common/ui/AnimatedGradient.kt
similarity index 89%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/AnimatedGradient.kt
rename to common/src/main/java/com/doyoonkim/common/ui/AnimatedGradient.kt
index eeca8b8f..e91e905e 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/AnimatedGradient.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/AnimatedGradient.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.LinearEasing
@@ -16,9 +16,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp
-import com.doyoonkim.knutice.ui.theme.animationGradientEnd
-import com.doyoonkim.knutice.ui.theme.animationGradientIntermediate
-import com.doyoonkim.knutice.ui.theme.animationGradientStart
+import com.doyoonkim.common.theme.animationGradientEnd
+import com.doyoonkim.common.theme.animationGradientIntermediate
+import com.doyoonkim.common.theme.animationGradientStart
@Composable
fun AnimatedGradient(
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/DateTimePicker.kt b/common/src/main/java/com/doyoonkim/common/ui/DateTimePicker.kt
similarity index 98%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/DateTimePicker.kt
rename to common/src/main/java/com/doyoonkim/common/ui/DateTimePicker.kt
index 77e17f33..5c698cd7 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/DateTimePicker.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/DateTimePicker.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import android.util.Log
import androidx.compose.foundation.background
@@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.doyoonkim.knutice.R
+import com.doyoonkim.common.R
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.LocalDateTime
diff --git a/common/src/main/java/com/doyoonkim/common/ui/LabeledToggleSwitch.kt b/common/src/main/java/com/doyoonkim/common/ui/LabeledToggleSwitch.kt
new file mode 100644
index 00000000..d851c435
--- /dev/null
+++ b/common/src/main/java/com/doyoonkim/common/ui/LabeledToggleSwitch.kt
@@ -0,0 +1,80 @@
+package com.doyoonkim.common.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.doyoonkim.common.theme.buttonPurple
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
+
+@Composable
+fun LabeledToggleSwitch(
+ modifier: Modifier = Modifier,
+ titleText: String = "Title Text",
+ subTitleText: String = "Subtitle Text",
+ isChecked: Boolean = false,
+ isEnabled: Boolean = false,
+ onCheckStatusChanged: (Boolean) -> Unit
+) {
+ Column(
+ modifier = modifier.fillMaxWidth().wrapContentSize()
+ .padding(top = 15.dp, bottom = 15.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(
+ modifier = Modifier.wrapContentHeight().weight(5f),
+ verticalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ text = titleText,
+ color = MaterialTheme.colorScheme.title,
+ fontWeight = FontWeight.Medium,
+ fontSize = 18.sp,
+ textAlign = TextAlign.Start
+ )
+
+ Text(
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ text = subTitleText,
+ color = MaterialTheme.colorScheme.subTitle,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ textAlign = TextAlign.Start
+ )
+ }
+
+ Switch(
+ checked = isChecked,
+ colors = SwitchDefaults.colors().copy(
+ checkedTrackColor = MaterialTheme.colorScheme.buttonPurple,
+ checkedThumbColor = Color.White
+ ),
+ onCheckedChange = {
+ onCheckStatusChanged(it)
+ },
+ enabled = isEnabled
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/LazyText.kt b/common/src/main/java/com/doyoonkim/common/ui/LazyText.kt
similarity index 96%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/LazyText.kt
rename to common/src/main/java/com/doyoonkim/common/ui/LazyText.kt
index fbdfe5e4..1f037a35 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/LazyText.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/LazyText.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreview.kt
similarity index 84%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt
rename to common/src/main/java/com/doyoonkim/common/ui/NotificationPreview.kt
index 86fa71df..a41084f6 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreview.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -18,19 +18,20 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
+import coil.request.CachePolicy
import coil.request.ImageRequest
-import com.doyoonkim.knutice.ui.theme.subTitle
-import com.doyoonkim.knutice.ui.theme.title
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
@Composable
fun NotificationPreview(
modifier: Modifier = Modifier,
+ isLoading: Boolean = true,
isImageContained: Boolean = false,
- notificationTitle: String = "Title goes here.",
+ notificationTitle: String = "Unknown",
notificationInfo: String = "Notification info goes here.",
imageUrl: String = ""
) {
@@ -38,7 +39,7 @@ fun NotificationPreview(
Modifier.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(7.dp)
) {
- if (notificationTitle == "Unknown") {
+ if (isLoading) {
AnimatedGradient(Modifier.height(24.dp))
AnimatedGradient(Modifier.height(14.dp))
} else {
@@ -50,6 +51,8 @@ fun NotificationPreview(
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
+ .diskCachePolicy(CachePolicy.DISABLED)
+ .memoryCachePolicy(CachePolicy.READ_ONLY)
.crossfade(true)
.build(),
contentDescription = "Contained Image",
@@ -64,7 +67,7 @@ fun NotificationPreview(
.padding(top = 7.dp, start = 5.dp, end = 5.dp),
text = notificationTitle,
textAlign = TextAlign.Start,
- fontSize = 14.sp,
+ fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.title,
maxLines = 1,
@@ -72,18 +75,12 @@ fun NotificationPreview(
)
Text(
modifier = Modifier.fillMaxWidth()
- .padding(start = 5.dp, bottom = 5.dp, end = 5.dp),
+ .padding(top = 1.dp, start = 5.dp, bottom = 5.dp, end = 5.dp),
text = notificationInfo,
textAlign = TextAlign.Start,
- fontSize = 12.sp,
+ fontSize = 9.sp,
color = MaterialTheme.colorScheme.subTitle
)
}
}
-}
-
-@Preview
-@Composable
-fun NotificationPreview_Preview() {
- NotificationPreview()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCard.kt
similarity index 83%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt
rename to common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCard.kt
index 4e9dbb66..55f0311d 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCard.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import android.content.res.Configuration
import androidx.compose.foundation.clickable
@@ -13,11 +13,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.doyoonkim.knutice.ui.theme.containerBackground
+import com.doyoonkim.common.theme.containerBackground
@Composable
fun NotificationPreviewCard(
modifier: Modifier = Modifier,
+ isLoading: Boolean = true,
notificationTitle: String = "Title goes here.",
notificationInfo: String = "Notification info goes here.",
onClick: () -> Unit = { /* Action should be defined. */ }
@@ -36,10 +37,11 @@ fun NotificationPreviewCard(
),
shape = RoundedCornerShape(10.dp)
) {
- NotificationPreview(
- notificationTitle = notificationTitle,
- notificationInfo = notificationInfo
- )
+ NotificationPreview(
+ notificationTitle = notificationTitle,
+ notificationInfo = notificationInfo,
+ isLoading = isLoading
+ )
}
}
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCardMarked.kt
similarity index 92%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt
rename to common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCardMarked.kt
index 2017e6a0..38660831 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/NotificationPreviewCardMarked.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
@@ -13,11 +13,12 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.doyoonkim.knutice.R
+import com.doyoonkim.common.R
@Composable
fun NotificationPreviewCardMarked(
modifier: Modifier = Modifier,
+ isLoading: Boolean = false,
noticeTitle: String = "Title goes here",
noticeSubtitle: String = "Subtitle goes here",
onItemClicked: () -> Unit = { },
@@ -28,6 +29,7 @@ fun NotificationPreviewCardMarked(
contentAlignment = Alignment.TopEnd
) {
NotificationPreviewCard(
+ isLoading = isLoading,
notificationTitle = noticeTitle,
notificationInfo = noticeSubtitle,
) {
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/PermissionRationaleComposable.kt b/common/src/main/java/com/doyoonkim/common/ui/PermissionRationaleComposable.kt
similarity index 90%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/PermissionRationaleComposable.kt
rename to common/src/main/java/com/doyoonkim/common/ui/PermissionRationaleComposable.kt
index a76c037b..3321d7cb 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/PermissionRationaleComposable.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/PermissionRationaleComposable.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
@@ -10,9 +10,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Surface
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -24,7 +25,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.doyoonkim.knutice.R
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.buttonPurple
@Composable
fun PermissionRationaleComposable(
@@ -70,6 +72,10 @@ fun PermissionRationaleComposable(
)
Button(
modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors().copy(
+ containerColor = MaterialTheme.colorScheme.buttonPurple,
+ contentColor = Color.White,
+ ),
onClick = onPermissionDecided
) {
Text(
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt b/common/src/main/java/com/doyoonkim/common/ui/PopUpDialog.kt
similarity index 90%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt
rename to common/src/main/java/com/doyoonkim/common/ui/PopUpDialog.kt
index 05b6a0a6..e9e2925e 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt
+++ b/common/src/main/java/com/doyoonkim/common/ui/PopUpDialog.kt
@@ -1,6 +1,5 @@
-package com.doyoonkim.knutice.presentation.component
+package com.doyoonkim.common.ui
-import android.graphics.Paint.Align
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
@@ -9,7 +8,6 @@ import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -26,17 +24,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role.Companion.Button
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.doyoonkim.knutice.R
-
-data class PopupDialogState(
- val isPopupDialogDisplayed: Boolean = false,
- val isPopupDialogMutedForDay: Boolean = true
-)
+import com.doyoonkim.common.R
@Composable
fun PopUpDialog(
@@ -114,5 +105,4 @@ fun PopUpDialog_Preview() {
}
}
}
-}
-
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_add_circle_24.xml b/common/src/main/res/drawable/baseline_add_circle_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_add_circle_24.xml
rename to common/src/main/res/drawable/baseline_add_circle_24.xml
diff --git a/app/src/main/res/drawable/baseline_arrow_back_ios_new_24.xml b/common/src/main/res/drawable/baseline_arrow_back_ios_new_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_arrow_back_ios_new_24.xml
rename to common/src/main/res/drawable/baseline_arrow_back_ios_new_24.xml
diff --git a/app/src/main/res/drawable/baseline_arrow_forward_ios_24.xml b/common/src/main/res/drawable/baseline_arrow_forward_ios_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_arrow_forward_ios_24.xml
rename to common/src/main/res/drawable/baseline_arrow_forward_ios_24.xml
diff --git a/app/src/main/res/drawable/baseline_bookmarks_24.xml b/common/src/main/res/drawable/baseline_bookmarks_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_bookmarks_24.xml
rename to common/src/main/res/drawable/baseline_bookmarks_24.xml
diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/common/src/main/res/drawable/baseline_close_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_close_24.xml
rename to common/src/main/res/drawable/baseline_close_24.xml
diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/common/src/main/res/drawable/baseline_home_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_home_24.xml
rename to common/src/main/res/drawable/baseline_home_24.xml
diff --git a/app/src/main/res/drawable/baseline_search_24.xml b/common/src/main/res/drawable/baseline_search_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_search_24.xml
rename to common/src/main/res/drawable/baseline_search_24.xml
diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/common/src/main/res/drawable/baseline_settings_24.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_settings_24.xml
rename to common/src/main/res/drawable/baseline_settings_24.xml
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/common/src/main/res/drawable/ic_launcher_background.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_launcher_background.xml
rename to common/src/main/res/drawable/ic_launcher_background.xml
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/common/src/main/res/drawable/ic_launcher_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_launcher_foreground.xml
rename to common/src/main/res/drawable/ic_launcher_foreground.xml
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/common/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to common/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/common/src/main/res/mipmap-anydpi/ic_launcher_round.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
rename to common/src/main/res/mipmap-anydpi/ic_launcher_round.xml
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/common/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher.png
rename to common/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/common/src/main/res/mipmap-hdpi/ic_launcher_background.png
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher_background.png
rename to common/src/main/res/mipmap-hdpi/ic_launcher_background.png
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/common/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
rename to common/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/common/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
rename to common/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/common/src/main/res/mipmap-hdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
rename to common/src/main/res/mipmap-hdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/common/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher.png
rename to common/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/common/src/main/res/mipmap-mdpi/ic_launcher_background.png
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher_background.png
rename to common/src/main/res/mipmap-mdpi/ic_launcher_background.png
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/common/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
rename to common/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/common/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
rename to common/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/common/src/main/res/mipmap-mdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
rename to common/src/main/res/mipmap-mdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/common/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to common/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/common/src/main/res/mipmap-xhdpi/ic_launcher_background.png
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
rename to common/src/main/res/mipmap-xhdpi/ic_launcher_background.png
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/common/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
rename to common/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/common/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
rename to common/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/common/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
rename to common/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/common/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to common/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/common/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
rename to common/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/common/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
rename to common/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/common/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
rename to common/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/common/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
rename to common/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/common/src/main/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
rename to common/src/main/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/common/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
rename to common/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/common/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
rename to common/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/common/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
rename to common/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/common/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
rename to common/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml
similarity index 97%
rename from app/src/main/res/values-ja/strings.xml
rename to common/src/main/res/values-ja/strings.xml
index 5c691206..56e9eef5 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/common/src/main/res/values-ja/strings.xml
@@ -61,4 +61,6 @@
More
KNUTICE Notice
選択したファイルをダウンロード フォルダーにダウンロードします。
+ 確認完了
+ 確認不可能
\ No newline at end of file
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/common/src/main/res/values-ko-rKR/strings.xml
similarity index 96%
rename from app/src/main/res/values-ko-rKR/strings.xml
rename to common/src/main/res/values-ko-rKR/strings.xml
index 96a1ff1b..62eaf17e 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/common/src/main/res/values-ko-rKR/strings.xml
@@ -61,4 +61,6 @@
한국어
번역 기능을 사용하려면 언어 모델을 다운로드 해야 합니다.
선택한 파일을 다운로드 폴더에 받고 있어요.
+ 저장이 완료되었어요.
+ 저장하는 중 문제가 생겼어요.
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml
similarity index 100%
rename from app/src/main/res/values/colors.xml
rename to common/src/main/res/values/colors.xml
diff --git a/app/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
similarity index 96%
rename from app/src/main/res/values/strings.xml
rename to common/src/main/res/values/strings.xml
index 78335f26..494e1464 100644
--- a/app/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -11,7 +11,7 @@
About
Version
Open Source License
- 1.4.1
+ 1.4.2
Notification
Preference
New Notice has been delivered!
@@ -71,4 +71,6 @@
English
Language model can be downloaded to use live translation
Downloading the selected file to Downloads...
+ Successfully saved.
+ Unable to save.
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/common/src/main/res/values/themes.xml
similarity index 100%
rename from app/src/main/res/values/themes.xml
rename to common/src/main/res/values/themes.xml
diff --git a/app/src/test/java/com/doyoonkim/knutice/ExampleUnitTest.kt b/common/src/test/java/com/doyoonkim/common/ExampleUnitTest.kt
similarity index 91%
rename from app/src/test/java/com/doyoonkim/knutice/ExampleUnitTest.kt
rename to common/src/test/java/com/doyoonkim/common/ExampleUnitTest.kt
index c13b244f..950d083d 100644
--- a/app/src/test/java/com/doyoonkim/knutice/ExampleUnitTest.kt
+++ b/common/src/test/java/com/doyoonkim/common/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice
+package com.doyoonkim.common
import org.junit.Test
diff --git a/core/data/.gitignore b/core/data/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/data/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
new file mode 100644
index 00000000..35d2c98c
--- /dev/null
+++ b/core/data/build.gradle.kts
@@ -0,0 +1,65 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+
+ id("kotlin-kapt")
+}
+
+android {
+ namespace = "com.doyoonkim.data"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 30
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(projects.core.model)
+ implementation(projects.core.network)
+ implementation(projects.core.domain) // Dependency Inversion
+ implementation(projects.common)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
+ // Coroutines for Android
+ implementation(libs.kotlinx.coroutines.android)
+
+ // Room Database
+ implementation(libs.androidx.room.runtime)
+ kapt(libs.androidx.room.compiler)
+ // Room Database - Kotlin Extensions and Coroutine Support
+ implementation(libs.androidx.room.ktx)
+}
\ No newline at end of file
diff --git a/core/data/consumer-rules.pro b/core/data/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/data/proguard-rules.pro b/core/data/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/data/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/core/data/src/androidTest/java/com/doyoonkim/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/doyoonkim/data/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..a492d76e
--- /dev/null
+++ b/core/data/src/androidTest/java/com/doyoonkim/data/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.data
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.data.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/core/data/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/data/src/main/java/com/doyoonkim/data/di/dataModule.kt b/core/data/src/main/java/com/doyoonkim/data/di/dataModule.kt
new file mode 100644
index 00000000..e42ea1b3
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/di/dataModule.kt
@@ -0,0 +1,69 @@
+package com.doyoonkim.data.di
+
+import android.content.Context
+import android.util.Log
+import androidx.room.Room
+import com.doyoonkim.common.di.ApplicationContext
+import com.doyoonkim.data.repository.ImageRepositoryImpl
+import com.doyoonkim.data.repository.LocalRepositoryImpl
+import com.doyoonkim.data.repository.RemoteRepositoryImpl
+import com.doyoonkim.data.room.LocalDatabase
+import com.doyoonkim.data.room.MainDatabaseDao
+import com.doyoonkim.domain.ImageRepository
+import com.doyoonkim.domain.LocalRepository
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.network.ImageRemoteSource
+import com.doyoonkim.network.KnuticeRemoteSource
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import javax.inject.Singleton
+
+@Module
+object DataModule {
+
+ // Database
+ @Provides
+ @Singleton
+ fun provideLocalDatabase(
+ @ApplicationContext context: Context
+ ): LocalDatabase {
+ // @Provides annotation is used because providing LocalDatabase instance requires custom-logic for instantiation.
+ return Room.databaseBuilder(
+ context,
+ LocalDatabase::class.java,
+ "Main Local Database"
+ ).setQueryCallback(
+ { sqlQuery, bindArgs ->
+ Log.d("SQL", "Query: $sqlQuery SQLArgs: $bindArgs")
+ },
+ Dispatchers.IO.asExecutor()
+ ).build()
+ }
+
+ @Provides
+ fun provideMainDatabaseDao(db: LocalDatabase) = db.getDao()
+
+ @Provides
+ fun provideLocalRepository(
+ localDao: MainDatabaseDao
+ ): LocalRepository {
+ return LocalRepositoryImpl(localDao)
+ }
+
+ @Provides
+ fun provideRemoteRepository(
+ remoteSource: KnuticeRemoteSource
+ ): RemoteRepository {
+ return RemoteRepositoryImpl(remoteSource)
+ }
+
+ @Provides
+ fun providesImageRepository(
+ imageRemoteSource: ImageRemoteSource
+ ): ImageRepository {
+ return ImageRepositoryImpl(imageRemoteSource)
+ }
+
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/doyoonkim/data/model/Bookmark.kt b/core/data/src/main/java/com/doyoonkim/data/model/Bookmark.kt
new file mode 100644
index 00000000..29b3970f
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/model/Bookmark.kt
@@ -0,0 +1,14 @@
+package com.doyoonkim.data.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class Bookmark(
+ @PrimaryKey(autoGenerate = true) val bookmarkId: Int = 0,
+ @ColumnInfo("isScheduled") val isScheduled: Boolean = false,
+ @ColumnInfo("remind_schedule") val reminderSchedule: Long = 0,
+ @ColumnInfo("bookmark_note") val note: String = "",
+ @ColumnInfo("target_ntt_id") val nttId: Int = -1
+)
diff --git a/core/data/src/main/java/com/doyoonkim/data/model/NoticeEntity.kt b/core/data/src/main/java/com/doyoonkim/data/model/NoticeEntity.kt
new file mode 100644
index 00000000..c5844a69
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/model/NoticeEntity.kt
@@ -0,0 +1,16 @@
+package com.doyoonkim.data.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class NoticeEntity(
+ @PrimaryKey(autoGenerate = true) val noticeEntityId: Int = 0,
+ @ColumnInfo("ntt_id") val nttId: Int = -1,
+ @ColumnInfo("notice_title") val title: String,
+ @ColumnInfo("notice_url") val url: String,
+ @ColumnInfo("notice_image") val imageUrl: String,
+ @ColumnInfo("info_dept") val departName: String,
+ @ColumnInfo("info_timestamp") val timestamp: String
+)
diff --git a/core/data/src/main/java/com/doyoonkim/data/repository/ImageRepositoryImpl.kt b/core/data/src/main/java/com/doyoonkim/data/repository/ImageRepositoryImpl.kt
new file mode 100644
index 00000000..3f549697
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/repository/ImageRepositoryImpl.kt
@@ -0,0 +1,32 @@
+package com.doyoonkim.data.repository
+
+import android.util.Log
+import com.doyoonkim.domain.ImageRepository
+import com.doyoonkim.network.ImageRemoteSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+/**
+ * @author kimdoyoon
+ * Created 6/17/25 at 12:41 AM
+ */
+class ImageRepositoryImpl @Inject constructor(
+ private val remoteSource: ImageRemoteSource
+) : ImageRepository {
+ override suspend fun getImageByteArrayFromUrl(url: String): ByteArray? {
+ remoteSource.getByteArrayFromImageUrl(url)
+ .fold(
+ onSuccess = {
+ return it
+ },
+ onFailure = {
+ Log.d(
+ "ImageRepositoryImpl",
+ "Unable to get ByteArray from the given url\nREASON: ${it.stackTrace}"
+ )
+ return null
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/doyoonkim/data/repository/LocalRepositoryImpl.kt b/core/data/src/main/java/com/doyoonkim/data/repository/LocalRepositoryImpl.kt
new file mode 100644
index 00000000..786643df
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/repository/LocalRepositoryImpl.kt
@@ -0,0 +1,151 @@
+package com.doyoonkim.data.repository
+
+import android.util.Log
+import com.doyoonkim.data.model.Bookmark
+import com.doyoonkim.data.model.NoticeEntity
+import com.doyoonkim.data.room.MainDatabaseDao
+import com.doyoonkim.domain.LocalRepository
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class LocalRepositoryImpl @Inject constructor(
+ // Inject Local Database from the app module (planned)
+ private val localDao: MainDatabaseDao
+) : LocalRepository {
+ private val TAG = "LocalRepositoryImpl"
+
+ // CRUD
+ override fun createBookmark(bookmark: BookmarkVO) = flow {
+ runCatching {
+ localDao.createBookmark(bookmark.toBookmark())
+ }.onFailure { throw it }.fold(
+ onSuccess = { emit(true) },
+ onFailure = { it.printLog().also { emit(false) } }
+ )
+ }
+
+ override fun createBookmark(bookmark: BookmarkVO, targetNotice: NoticeVO) = flow {
+ runCatching {
+ localDao.run {
+ // Insert Notice Entity First
+ createNoticeEntity(targetNotice.toNoticeEntity())
+ // Insert Bookmark Entity next.
+ this.createBookmark(bookmark.toBookmark())
+ }
+ }.onFailure { throw it }.fold(
+ onSuccess = { emit(true) },
+ onFailure = { it.printLog().also { emit(false) } }
+ )
+ }
+
+ override fun updateBookmark(bookmark: BookmarkVO) = flow {
+ runCatching {
+ localDao.updateBookmark(bookmark.toBookmark())
+ }.onFailure { throw it }.fold(
+ onSuccess = { emit(true) },
+ onFailure = { it.printLog().also { emit(false) } }
+ )
+ }
+
+ override fun queryAllBookmarks() = flow {
+ runCatching {
+ localDao.getAllBookmarks()
+ }.onFailure { throw it }.fold(
+ onSuccess = {
+ Log.d(TAG, "${it.size}")
+ it.toListOfBookmarkVO().forEach {
+ vo -> emit(vo).also { Log.d(TAG, vo.toString()) }
+ } },
+ onFailure = { it.printLog().also { emit(null) } }
+ )
+ }
+
+ override fun queryNoticeById(nttId: Int) = flow {
+ runCatching {
+ localDao.getNoticeByNttId(nttId)
+ }.onFailure { throw it }.fold(
+ onSuccess = {
+ if (it != null) emit(it.toNoticeVO())
+ else emit(null)
+ },
+ onFailure = { it.printLog().also { emit(null) } }
+ )
+ }
+
+ override fun queryBookmarkByNttId(nttId: Int) = flow {
+ runCatching {
+ localDao.getBookmarkByNttId(nttId)
+ }.onFailure { throw it }.fold(
+ onSuccess = {
+ emit(it?.toBookmarkVO())
+ },
+ onFailure = { emit(null) }
+ )
+ }
+
+ override fun requestBookmarkDeletion(bookmark: BookmarkVO) = flow {
+ runCatching {
+ localDao.deleteBookmark(bookmark.toBookmark())
+ }.onFailure { throw it }.fold(
+ onSuccess = { emit(true) },
+ onFailure = { emit(false) }
+ )
+ }
+
+ override fun requestNoticeDeletion(notice: NoticeVO) = flow {
+ runCatching {
+ localDao.deleteNoticeEntity(notice.toNoticeEntity())
+ }.onFailure { throw it }.fold(
+ onSuccess = { emit(true) },
+ onFailure = { emit(false) }
+ )
+ }
+
+
+ private fun Throwable.printLog() =
+ Log.d(TAG, "Failed to receive data\nREASON: ${this.stackTraceToString()}")
+
+ private fun NoticeVO.toNoticeEntity() =
+ NoticeEntity(
+ nttId = this.nttId,
+ title = this.title,
+ url = this.url,
+ imageUrl = this.imageUrl ?: "",
+ departName = this.departName,
+ timestamp = this.timestamp
+ )
+
+ private fun NoticeEntity.toNoticeVO() =
+ NoticeVO(
+ entityId = this.noticeEntityId,
+ nttId = this.nttId,
+ title = this.title,
+ url = this.url,
+ imageUrl = this.imageUrl,
+ departName = this.departName,
+ timestamp = this.timestamp
+ )
+
+ private fun BookmarkVO.toBookmark() =
+ Bookmark(
+ bookmarkId = this.bookmarkId,
+ nttId = this.targetNoticeNttId,
+ isScheduled = this.isScheduled,
+ reminderSchedule = this.reminderSchedule,
+ note = this.bookmarkNote
+ )
+
+ private fun Bookmark.toBookmarkVO() =
+ BookmarkVO(
+ bookmarkId = this.bookmarkId,
+ targetNoticeNttId = this.nttId,
+ isScheduled = this.isScheduled,
+ reminderSchedule = this.reminderSchedule,
+ bookmarkNote = this.note
+ )
+
+ private fun List.toListOfBookmarkVO() =
+ this.map { it.toBookmarkVO() }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..c7384868
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/repository/RemoteRepositoryImpl.kt
@@ -0,0 +1,152 @@
+package com.doyoonkim.data.repository
+
+import android.util.Log
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.model.requestBody.DeviceTokenBody
+import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody
+import com.doyoonkim.model.requestBody.UserReportBody
+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.flow.flow
+import model.NetworkResult
+import javax.inject.Inject
+
+class RemoteRepositoryImpl @Inject constructor(
+ private val remoteSource: KnuticeRemoteSource
+) : RemoteRepository {
+ private val TAG = "RemoteRepositoryImpl"
+
+ override fun queryTopThreeNotices() = flow {
+ remoteSource.getTopThreeNotices().fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(it.body?.toVO())
+ else it.result.printLog().also { emit(null) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(null)
+ }
+ )
+ }
+
+ override fun queryNoticesPerPage(category: NoticeCategory, lastNttId: Int?) = flow {
+ remoteSource.getNoticesPerPage(category, lastNttId).fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(it.body.map { it.toVO() })
+ else it.result.printLog().also { emit(null) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(null)
+ }
+ )
+ }
+
+ override fun queryNoticeById(nttId: Int) = flow {
+ remoteSource.getNoticeById(nttId).fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(it.body?.toVO())
+ else it.result.printLog().also { emit(null) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(null)
+ }
+ )
+ }
+
+ override fun queryNoticesByKeyword(keyword: String) = flow {
+ remoteSource.getNoticesByKeyword(keyword).fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(it.body.map { it.toVO() })
+ else it.result.printLog().also { emit(null) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(null)
+ }
+ )
+ }
+
+ override fun queryTopicSubscriptionStatus() = flow {
+ remoteSource.getTopicSubscriptionStatus().fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(it.body?.toVO())
+ else {
+ if (it.body != null) emit(it.body?.toVO())
+ else it.result.printLog().also { emit(null) }
+ }
+ },
+ onFailure = {
+ it.printLog()
+ emit(null)
+ }
+ )
+ }
+
+ override fun requestTokenValidation(body: DeviceTokenBody) = flow {
+ remoteSource.validateToken(
+ DeviceTokenRequest(body = body)
+ ).fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(true).also {
+ remoteSource.updateValidatedToken(body.fcmToken)
+ }
+ else it.result.printLog().also { emit(false) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(false)
+ }
+ )
+ }
+
+ override fun requestUpdateValidatedToken(fcmToken: String) {
+ remoteSource.updateValidatedToken(fcmToken)
+ }
+
+ override fun requestUserReportSubmission(body: UserReportBody) = flow {
+ remoteSource.submitUserReport(
+ // Should be revised.
+ UserReportRequest(body = body.copy(fcmToken = remoteSource.validatedToken))
+ ).fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(true)
+ else it.result.printLog().also { emit(false) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(false)
+ }
+ )
+ }
+
+ override fun requestTopicSubscriptionPreferencesSubmission(
+ body: TopicSubscriptionPreferencesBody
+ ) = flow {
+ remoteSource.run {
+ submitTopicSubscriptionPreferences(
+ TopicSubscriptionPreferencesRequest(body = body.copy(fcmToken = this.validatedToken))
+ )
+ }.fold(
+ onSuccess = {
+ if (it.result?.resultCode == 200) emit(true)
+ else it.result.printLog().also { emit(false) }
+ },
+ onFailure = {
+ it.printLog()
+ emit(false)
+ }
+ )
+ }
+
+ private fun Throwable.printLog() =
+ Log.d(TAG, "Failed to receive data\nREASON: ${this.stackTraceToString()}")
+
+ private fun NetworkResult?.printLog() =
+ Log.d(TAG, "Failed to receive data (${this?.resultCode})" +
+ "\nREASON:${this?.resultMessage}")
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/doyoonkim/data/room/LocalRoomDatabase.kt b/core/data/src/main/java/com/doyoonkim/data/room/LocalRoomDatabase.kt
new file mode 100644
index 00000000..6f28c99d
--- /dev/null
+++ b/core/data/src/main/java/com/doyoonkim/data/room/LocalRoomDatabase.kt
@@ -0,0 +1,13 @@
+package com.doyoonkim.data.room
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.doyoonkim.data.model.Bookmark
+import com.doyoonkim.data.model.NoticeEntity
+
+// Migration Strategy: https://stackoverflow.com/questions/56478785/room-database-schema-update-without-data-loss
+
+@Database(entities = [Bookmark::class, NoticeEntity::class], version = 1)
+abstract class LocalDatabase : RoomDatabase() {
+ abstract fun getDao(): MainDatabaseDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt b/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt
similarity index 81%
rename from app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt
rename to core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt
index c8606812..f8dc9dd2 100644
--- a/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt
+++ b/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt
@@ -1,4 +1,4 @@
-package com.doyoonkim.knutice.data.local
+package com.doyoonkim.data.room
import androidx.room.Dao
import androidx.room.Delete
@@ -6,8 +6,8 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
-import com.doyoonkim.knutice.model.Bookmark
-import com.doyoonkim.knutice.model.NoticeEntity
+import com.doyoonkim.data.model.Bookmark
+import com.doyoonkim.data.model.NoticeEntity
@Dao
interface MainDatabaseDao {
@@ -30,7 +30,7 @@ interface MainDatabaseDao {
fun deleteNoticeEntity(target: NoticeEntity)
@Query("SELECT * FROM NoticeEntity WHERE ntt_id=:nttId")
- fun getNoticeByNttId(nttId: Int): NoticeEntity
+ fun getNoticeByNttId(nttId: Int): NoticeEntity?
@Query("SELECT * FROM Bookmark WHERE target_ntt_id=:nttId")
fun getBookmarkByNttId(nttId: Int): Bookmark?
diff --git a/core/data/src/test/java/com/doyoonkim/data/ExampleUnitTest.kt b/core/data/src/test/java/com/doyoonkim/data/ExampleUnitTest.kt
new file mode 100644
index 00000000..24412861
--- /dev/null
+++ b/core/data/src/test/java/com/doyoonkim/data/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.data
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/core/domain/.gitignore b/core/domain/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/domain/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
new file mode 100644
index 00000000..8fa67eda
--- /dev/null
+++ b/core/domain/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ id("java-library")
+ alias(libs.plugins.jetbrains.kotlin.jvm)
+ id("kotlin-kapt")
+}
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+ }
+}
+
+dependencies {
+ implementation(projects.core.model)
+
+ implementation(libs.kotlinx.coroutines.core)
+
+ // Dagger
+ implementation(libs.dagger)
+ kapt(libs.dagger.compiler)
+}
diff --git a/core/domain/src/main/java/com/doyoonkim/domain/ImageRepository.kt b/core/domain/src/main/java/com/doyoonkim/domain/ImageRepository.kt
new file mode 100644
index 00000000..c79660ac
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/ImageRepository.kt
@@ -0,0 +1,11 @@
+package com.doyoonkim.domain
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * @author kimdoyoon
+ * Created 6/17/25 at 12:40 AM
+ */
+interface ImageRepository {
+ suspend fun getImageByteArrayFromUrl(url: String): ByteArray?
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/doyoonkim/domain/LocalRepository.kt b/core/domain/src/main/java/com/doyoonkim/domain/LocalRepository.kt
new file mode 100644
index 00000000..38f9d6b6
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/LocalRepository.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.domain
+
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+
+// Dependency Inversion
+interface LocalRepository {
+ fun createBookmark(bookmark: BookmarkVO): Flow
+
+ fun createBookmark(bookmark: BookmarkVO, targetNotice: NoticeVO): Flow
+
+ fun updateBookmark(bookmark: BookmarkVO): Flow
+
+ fun queryAllBookmarks(): Flow
+
+ fun queryNoticeById(nttId: Int): Flow
+
+ fun queryBookmarkByNttId(nttId: Int): Flow
+
+ fun requestBookmarkDeletion(bookmark: BookmarkVO): Flow
+
+ fun requestNoticeDeletion(notice: NoticeVO): Flow
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/doyoonkim/domain/RemoteRepository.kt b/core/domain/src/main/java/com/doyoonkim/domain/RemoteRepository.kt
new file mode 100644
index 00000000..37b72a7d
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/RemoteRepository.kt
@@ -0,0 +1,34 @@
+package com.doyoonkim.domain
+
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.model.NoticeVO
+import com.doyoonkim.model.TopThreeNoticeVO
+import com.doyoonkim.model.TopicSubscriptionPreferencesVO
+import com.doyoonkim.model.requestBody.DeviceTokenBody
+import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody
+import com.doyoonkim.model.requestBody.UserReportBody
+import kotlinx.coroutines.flow.Flow
+
+// Dependency Inversion
+interface RemoteRepository {
+ fun queryTopThreeNotices(): Flow
+
+ fun queryNoticesPerPage(category: NoticeCategory, lastNttId: Int?): Flow?>
+
+ fun queryNoticeById(nttId: Int): Flow
+
+ fun queryNoticesByKeyword(keyword: String): Flow?>
+
+ fun queryTopicSubscriptionStatus(): Flow
+
+ fun requestTokenValidation(body: DeviceTokenBody): Flow
+
+ fun requestUpdateValidatedToken(fcmToken: String)
+
+ fun requestUserReportSubmission(body: UserReportBody): Flow
+
+ fun requestTopicSubscriptionPreferencesSubmission(
+ body: TopicSubscriptionPreferencesBody
+ ): Flow
+
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/doyoonkim/domain/di/DomainModule.kt b/core/domain/src/main/java/com/doyoonkim/domain/di/DomainModule.kt
new file mode 100644
index 00000000..78eddcae
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/di/DomainModule.kt
@@ -0,0 +1,86 @@
+package com.doyoonkim.domain.di
+
+import com.doyoonkim.domain.usecases.FetchAllBookmarks
+import com.doyoonkim.domain.usecases.FetchAllBookmarksImpl
+import com.doyoonkim.domain.usecases.FetchNoticeById
+import com.doyoonkim.domain.usecases.FetchNoticeByIdFromLocal
+import com.doyoonkim.domain.usecases.FetchNoticeByIdFromLocalImpl
+import com.doyoonkim.domain.usecases.FetchNoticeByIdImpl
+import com.doyoonkim.domain.usecases.FetchNoticesByKeyword
+import com.doyoonkim.domain.usecases.FetchNoticesByKeywordImpl
+import com.doyoonkim.domain.usecases.FetchNoticesPerPage
+import com.doyoonkim.domain.usecases.FetchNoticesPerPageImpl
+import com.doyoonkim.domain.usecases.FetchTopThreeNotices
+import com.doyoonkim.domain.usecases.FetchTopThreeNoticesImpl
+import com.doyoonkim.domain.usecases.FetchTopicSubscriptionStatus
+import com.doyoonkim.domain.usecases.FetchTopicSubscriptionStatusImpl
+import com.doyoonkim.domain.usecases.ModifyBookmark
+import com.doyoonkim.domain.usecases.ModifyBookmarkImpl
+import com.doyoonkim.domain.usecases.SubmitNotificationPreferences
+import com.doyoonkim.domain.usecases.SubmitNotificationPreferencesImpl
+import com.doyoonkim.domain.usecases.SubmitUserReport
+import com.doyoonkim.domain.usecases.SubmitUserReportImpl
+import com.doyoonkim.domain.usecases.ValidateDeviceToken
+import com.doyoonkim.domain.usecases.ValidateDeviceTokenImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class DomainModule {
+
+ @Binds
+ abstract fun bindFetchAllBookmarks(
+ impl: FetchAllBookmarksImpl
+ ): FetchAllBookmarks
+
+ @Binds
+ abstract fun bindsFetchNoticeById(
+ impl: FetchNoticeByIdImpl
+ ): FetchNoticeById
+
+ @Binds
+ abstract fun bindsFetchNoticeByIdFromLocal(
+ impl: FetchNoticeByIdFromLocalImpl
+ ): FetchNoticeByIdFromLocal
+
+ @Binds
+ abstract fun bindsFetchNoticesByKeyword(
+ impl: FetchNoticesByKeywordImpl
+ ): FetchNoticesByKeyword
+
+ @Binds
+ abstract fun bindsFetchNoticesPerPage(
+ impl: FetchNoticesPerPageImpl
+ ): FetchNoticesPerPage
+
+ @Binds
+ abstract fun bindsFetchTopicSubscriptionStatus(
+ impl: FetchTopicSubscriptionStatusImpl
+ ): FetchTopicSubscriptionStatus
+
+ @Binds
+ abstract fun bindsFetchTopThreeNotices(
+ impl: FetchTopThreeNoticesImpl
+ ): FetchTopThreeNotices
+
+ @Binds
+ abstract fun bindsModifyBookmark(
+ impl: ModifyBookmarkImpl
+ ): ModifyBookmark
+
+ @Binds
+ abstract fun bindsSubmitNotificationPReferences(
+ impl: SubmitNotificationPreferencesImpl
+ ): SubmitNotificationPreferences
+
+ @Binds
+ abstract fun bindsSubmitUserReport(
+ impl: SubmitUserReportImpl
+ ): SubmitUserReport
+
+ @Binds
+ abstract fun bindsValidateDeviceToken(
+ impl: ValidateDeviceTokenImpl
+ ): ValidateDeviceToken
+
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..730e6656
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchAllBookmarks.kt
@@ -0,0 +1,29 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.LocalRepository
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+
+interface FetchAllBookmarks {
+ operator fun invoke(): Flow>
+}
+
+class FetchAllBookmarksImpl @Inject constructor(
+ private val localRepository: LocalRepository
+) : FetchAllBookmarks {
+
+ override operator fun invoke(): Flow> =
+ localRepository.queryAllBookmarks().transform { bookmarkVO ->
+ bookmarkVO?.let {
+ emitAll(localRepository.queryNoticeById(it.targetNoticeNttId).transform { nullable ->
+ nullable?.let { vo-> emit(Pair(bookmarkVO, vo)) }
+ })
+ }
+ }
+
+}
\ 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
new file mode 100644
index 00000000..39173253
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeById.kt
@@ -0,0 +1,21 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchNoticeById {
+ operator fun invoke(nttId: Int): Flow
+}
+
+class FetchNoticeByIdImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : FetchNoticeById {
+
+ override operator fun invoke(nttId: Int) =
+ remoteRepository.queryNoticeById(nttId).transform { result ->
+ result?.let { emit(it) }
+ }
+}
\ 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
new file mode 100644
index 00000000..cb9a1998
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticeByIdFromLocal.kt
@@ -0,0 +1,22 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.LocalRepository
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchNoticeByIdFromLocal {
+ operator fun invoke(nttId: Int): Flow
+}
+
+class FetchNoticeByIdFromLocalImpl @Inject constructor(
+ private val localRepository: LocalRepository
+) : FetchNoticeByIdFromLocal {
+
+ override operator fun invoke(nttId: Int) =
+ localRepository.queryNoticeById(nttId).transform { result ->
+ result?.let { emit(it) }
+ }
+
+}
\ 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
new file mode 100644
index 00000000..8f9b36bb
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesByKeyword.kt
@@ -0,0 +1,21 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchNoticesByKeyword {
+ operator fun invoke (keyword: String): Flow>
+}
+
+class FetchNoticesByKeywordImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : FetchNoticesByKeyword {
+
+ override operator fun invoke(keyword: String) =
+ remoteRepository.queryNoticesByKeyword(keyword).transform { result ->
+ result?.let { emit(it) }
+ }
+}
\ 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
new file mode 100644
index 00000000..511683c2
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchNoticesPerPage.kt
@@ -0,0 +1,25 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchNoticesPerPage {
+ operator fun invoke(category: NoticeCategory, lastNttId: Int): Flow>
+}
+
+class FetchNoticesPerPageImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : FetchNoticesPerPage {
+
+ override operator fun invoke(category: NoticeCategory, lastNttId: Int) =
+ remoteRepository.run {
+ if (lastNttId == 0) queryNoticesPerPage(category, null)
+ else queryNoticesPerPage(category, lastNttId)
+ }.transform { result ->
+ result?.let { emit(it) }
+ }
+}
\ 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
new file mode 100644
index 00000000..696d6ab0
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopThreeNotices.kt
@@ -0,0 +1,22 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.TopThreeNoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchTopThreeNotices {
+ operator fun invoke(): Flow
+}
+
+class FetchTopThreeNoticesImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : FetchTopThreeNotices {
+
+ override operator fun invoke(): Flow =
+ remoteRepository.queryTopThreeNotices().transform { result ->
+ result?.let { emit(it) }
+ }
+
+}
\ 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
new file mode 100644
index 00000000..8f226ddd
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/FetchTopicSubscriptionStatus.kt
@@ -0,0 +1,22 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.TopicSubscriptionPreferencesVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface FetchTopicSubscriptionStatus {
+ operator fun invoke(): Flow
+}
+
+class FetchTopicSubscriptionStatusImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : FetchTopicSubscriptionStatus {
+
+ override operator fun invoke() =
+ remoteRepository.queryTopicSubscriptionStatus().transform { nullable ->
+ nullable?.let { emit(it) }
+ }
+
+}
\ 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
new file mode 100644
index 00000000..37089257
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ModifyBookmark.kt
@@ -0,0 +1,51 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.LocalRepository
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.transform
+import javax.inject.Inject
+
+interface ModifyBookmark {
+ // Contains function for query, update, delete of Bookmark
+ fun query(nttId: Int): Flow
+
+ fun createOrUpdate(bookmark: BookmarkVO, notice: NoticeVO?): Flow
+
+ fun delete(bookmark: BookmarkVO, notice: NoticeVO): Flow
+}
+
+class ModifyBookmarkImpl @Inject constructor(
+ private val localRepository: LocalRepository,
+ private val remoteRepository: RemoteRepository
+) : ModifyBookmark {
+
+ override fun query(nttId: Int): Flow =
+ localRepository.queryBookmarkByNttId(nttId).transform { result ->
+ result?.let { emit(it) }
+ }
+
+ // TODO: Need to be revised later. (Is NoticeVO really required)
+ override fun createOrUpdate(bookmark: BookmarkVO, notice: NoticeVO?): Flow {
+ return if (notice == null) {
+ // Need creation. Request notice instance from the remote source first.
+ remoteRepository.queryNoticeById(bookmark.targetNoticeNttId).transform { result ->
+ if (result != null) emitAll(localRepository.createBookmark(bookmark, result))
+ else emit(false)
+ }
+ } else {
+ localRepository.updateBookmark(bookmark)
+ }
+ }
+
+ override fun delete(bookmark: BookmarkVO, notice: NoticeVO): Flow =
+ localRepository.requestBookmarkDeletion(bookmark).transform { result ->
+ if (result) emitAll(localRepository.requestNoticeDeletion(notice))
+ else emit(false)
+ }
+
+}
+
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
new file mode 100644
index 00000000..1d891f3e
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitNotificationPreferences.kt
@@ -0,0 +1,18 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+interface SubmitNotificationPreferences {
+ operator fun invoke(body: TopicSubscriptionPreferencesBody): Flow
+}
+
+class SubmitNotificationPreferencesImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : SubmitNotificationPreferences {
+
+ override operator fun invoke(body: TopicSubscriptionPreferencesBody) =
+ remoteRepository.requestTopicSubscriptionPreferencesSubmission(body)
+}
\ 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
new file mode 100644
index 00000000..6099c0f8
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/SubmitUserReport.kt
@@ -0,0 +1,19 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.requestBody.UserReportBody
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+interface SubmitUserReport {
+ operator fun invoke(body: UserReportBody): Flow
+}
+
+class SubmitUserReportImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : SubmitUserReport {
+
+ override operator fun invoke(body: UserReportBody) =
+ remoteRepository.requestUserReportSubmission(body)
+
+}
\ 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
new file mode 100644
index 00000000..3c9f694c
--- /dev/null
+++ b/core/domain/src/main/java/com/doyoonkim/domain/usecases/ValidateDeviceToken.kt
@@ -0,0 +1,19 @@
+package com.doyoonkim.domain.usecases
+
+import com.doyoonkim.domain.RemoteRepository
+import com.doyoonkim.model.requestBody.DeviceTokenBody
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+
+interface ValidateDeviceToken {
+ operator fun invoke(requestBody: DeviceTokenBody): Flow
+}
+
+class ValidateDeviceTokenImpl @Inject constructor(
+ private val remoteRepository: RemoteRepository
+) : ValidateDeviceToken {
+
+ override operator fun invoke(requestBody: DeviceTokenBody) =
+ remoteRepository.requestTokenValidation(requestBody)
+}
\ No newline at end of file
diff --git a/core/model/.gitignore b/core/model/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/model/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
new file mode 100644
index 00000000..864aec72
--- /dev/null
+++ b/core/model/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ id("java-library")
+ alias(libs.plugins.jetbrains.kotlin.jvm)
+}
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+ }
+}
+
+dependencies {
+ implementation(libs.kotlin.serialization)
+}
diff --git a/core/model/src/main/java/com/doyoonkim/model/BookmarkVO.kt b/core/model/src/main/java/com/doyoonkim/model/BookmarkVO.kt
new file mode 100644
index 00000000..b98988fb
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/BookmarkVO.kt
@@ -0,0 +1,10 @@
+package com.doyoonkim.model
+
+data class BookmarkVO(
+ val bookmarkId: Int = 0,
+ // nttId is being used as ID for local Push Notification as well.
+ val targetNoticeNttId: Int,
+ val isScheduled: Boolean,
+ val reminderSchedule: Long,
+ val bookmarkNote: String
+)
diff --git a/core/model/src/main/java/com/doyoonkim/model/NoticeCatetory.kt b/core/model/src/main/java/com/doyoonkim/model/NoticeCatetory.kt
new file mode 100644
index 00000000..68677000
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/NoticeCatetory.kt
@@ -0,0 +1,14 @@
+package com.doyoonkim.model
+
+/**
+ * @author kimdoyoon
+ * Created 6/2/25 at 11:20 PM
+ */
+enum class NoticeCategory {
+ GENERAL_NEWS,
+ ACADEMIC_NEWS,
+ SCHOLARSHIP_NEWS,
+ EVENT_NEWS,
+ JOB_NEWS,
+ Unspecified
+}
\ No newline at end of file
diff --git a/core/model/src/main/java/com/doyoonkim/model/NoticeVO.kt b/core/model/src/main/java/com/doyoonkim/model/NoticeVO.kt
new file mode 100644
index 00000000..16875e94
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/NoticeVO.kt
@@ -0,0 +1,11 @@
+package com.doyoonkim.model
+
+data class NoticeVO(
+ val entityId: Int? = null, // Later migrated and removed.
+ val nttId: Int = -1,
+ val title: String = "",
+ val url: String = "",
+ val imageUrl: String? = null,
+ val departName: String = "",
+ val timestamp: String = ""
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/doyoonkim/model/TopThreeNoticeVO.kt b/core/model/src/main/java/com/doyoonkim/model/TopThreeNoticeVO.kt
new file mode 100644
index 00000000..75404a93
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/TopThreeNoticeVO.kt
@@ -0,0 +1,8 @@
+package com.doyoonkim.model
+
+data class TopThreeNoticeVO(
+ val general: List = listOf(),
+ val scholarship: List = listOf(),
+ val event: List = listOf(),
+ val academic: List = listOf()
+)
diff --git a/core/model/src/main/java/com/doyoonkim/model/TopicSubscriptionPreferencesVO.kt b/core/model/src/main/java/com/doyoonkim/model/TopicSubscriptionPreferencesVO.kt
new file mode 100644
index 00000000..66231fe1
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/TopicSubscriptionPreferencesVO.kt
@@ -0,0 +1,9 @@
+package com.doyoonkim.model
+
+data class TopicSubscriptionPreferencesVO(
+ val general: Boolean = false,
+ val scholarship: Boolean = false,
+ val event: Boolean = false,
+ val academic: Boolean = false,
+ val employment: Boolean = false
+)
diff --git a/core/model/src/main/java/com/doyoonkim/model/requestBody/DeviceTokenBody.kt b/core/model/src/main/java/com/doyoonkim/model/requestBody/DeviceTokenBody.kt
new file mode 100644
index 00000000..3563f060
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/requestBody/DeviceTokenBody.kt
@@ -0,0 +1,5 @@
+package com.doyoonkim.model.requestBody
+
+data class DeviceTokenBody(
+ val fcmToken: String
+)
diff --git a/core/model/src/main/java/com/doyoonkim/model/requestBody/TopicSubscriptionPreferencesBody.kt b/core/model/src/main/java/com/doyoonkim/model/requestBody/TopicSubscriptionPreferencesBody.kt
new file mode 100644
index 00000000..fad0dff0
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/requestBody/TopicSubscriptionPreferencesBody.kt
@@ -0,0 +1,7 @@
+package com.doyoonkim.model.requestBody
+
+data class TopicSubscriptionPreferencesBody(
+ val fcmToken: String? = null,
+ val noticeName: String = "Unspecified",
+ val isSubscribed: Boolean = false
+)
diff --git a/core/model/src/main/java/com/doyoonkim/model/requestBody/UserReportBody.kt b/core/model/src/main/java/com/doyoonkim/model/requestBody/UserReportBody.kt
new file mode 100644
index 00000000..3aaf196d
--- /dev/null
+++ b/core/model/src/main/java/com/doyoonkim/model/requestBody/UserReportBody.kt
@@ -0,0 +1,9 @@
+package com.doyoonkim.model.requestBody
+
+data class UserReportBody(
+ val fcmToken: String? = null,
+ val content: String = "",
+ val clientType: String = "APP",
+ val deviceName: String = "Android Device",
+ val version: String = "Unspecified"
+)
diff --git a/core/network/.gitignore b/core/network/.gitignore
new file mode 100644
index 00000000..c0a79076
--- /dev/null
+++ b/core/network/.gitignore
@@ -0,0 +1,4 @@
+/build
+
+# Local Properties
+local.properties
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
new file mode 100644
index 00000000..8936b327
--- /dev/null
+++ b/core/network/build.gradle.kts
@@ -0,0 +1,73 @@
+import java.io.FileInputStream
+import java.util.Properties
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ id("kotlin-kapt")
+}
+
+android {
+ namespace = "com.doyoonkim.network"
+ compileSdk = 35
+
+ val properties = Properties().apply {
+ load(FileInputStream("${rootDir}/local.properties"))
+ }
+ val apiBaseLive = properties["api_migrated"] ?: ""
+
+ defaultConfig {
+ minSdk = 30
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+
+ buildConfigField("String", "API_LIVE", "\"$apiBaseLive\"")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+dependencies {
+ implementation(projects.core.model)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinx.coroutines.test)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ // Coroutine for Android
+ implementation(libs.kotlinx.coroutines.android)
+
+ // Retrofit 2
+ implementation(libs.retrofit)
+ implementation(libs.converter.gson)
+
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+}
\ No newline at end of file
diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/network/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/core/network/src/androidTest/java/com/doyoonkim/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/doyoonkim/network/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..62ff60bf
--- /dev/null
+++ b/core/network/src/androidTest/java/com/doyoonkim/network/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.network
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.network.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/core/network/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/ImageRemoteSource.kt b/core/network/src/main/kotlin/com/doyoonkim/network/ImageRemoteSource.kt
new file mode 100644
index 00000000..c0158270
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/ImageRemoteSource.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.network
+
+import java.net.URL
+import javax.inject.Inject
+
+/**
+ * @author kimdoyoon
+ * Created 6/17/25 at 12:42 AM
+ */
+class ImageRemoteSource @Inject constructor() {
+ suspend fun getByteArrayFromImageUrl(url: String): Result {
+ return runCatching {
+ URL(url).readBytes()
+ }.onFailure { throw it }
+ }
+
+}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/KnuticeRemoteSource.kt b/core/network/src/main/kotlin/com/doyoonkim/network/KnuticeRemoteSource.kt
new file mode 100644
index 00000000..84eb8092
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/KnuticeRemoteSource.kt
@@ -0,0 +1,72 @@
+package com.doyoonkim.network
+
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.network.model.DeviceTokenRequest
+import com.doyoonkim.network.model.TopicSubscriptionPreferencesRequest
+import com.doyoonkim.network.model.UserReportRequest
+import com.doyoonkim.network.retrofit.KnuticeService
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * @author kimdoyoon
+ * Created 6/3/25 at 12:27 AM
+ */
+
+// This class should be provided/injected as Singleton Instance.
+@Singleton
+class KnuticeRemoteSource @Inject constructor() {
+ private val TAG = "KnuticeRemoteSource"
+
+ // Consider providing this instance via DI.
+ private val knuticeService = Retrofit.Builder()
+ .baseUrl(BuildConfig.API_LIVE)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+ // Should be later migrated to DataStore.
+ var validatedToken: String = ""
+
+ suspend fun getTopThreeNotices() = runCatching {
+ knuticeService.create(KnuticeService::class.java).getTopThreeNotices()
+ }.onFailure { throw it }
+
+ suspend fun getNoticesPerPage(category: NoticeCategory, lastNttId: Int?) = runCatching {
+ knuticeService.create(KnuticeService::class.java).getNoticesPerPage(category, lastNttId)
+ }.onFailure { throw it }
+
+ suspend fun getNoticeById(nttId: Int) = runCatching {
+ knuticeService.create(KnuticeService::class.java).getNoticeById(nttId.toString())
+ }.onFailure { throw it }
+
+
+ suspend fun getNoticesByKeyword(keyword: String) = runCatching {
+ knuticeService.create(KnuticeService::class.java).getNoticesByKeyword(keyword)
+ }.onFailure { throw it }
+
+ suspend fun getTopicSubscriptionStatus() = runCatching {
+ if (validatedToken.isBlank()) throw Exception("No validated token found")
+ else {
+ knuticeService.create(KnuticeService::class.java).getTopicSubscriptionStatus(validatedToken)
+ }
+ }.onFailure { throw it }
+
+ suspend fun validateToken(request: DeviceTokenRequest) = runCatching {
+ knuticeService.create(KnuticeService::class.java).validateToken(request)
+ }.onFailure { throw it }
+
+ fun updateValidatedToken(fcmToken: String) {
+ validatedToken = fcmToken
+ }
+
+ suspend fun submitUserReport(request: UserReportRequest) = runCatching {
+ knuticeService.create(KnuticeService::class.java).submitUserReport(request)
+ }.onFailure { throw it }
+
+ suspend fun submitTopicSubscriptionPreferences(request: TopicSubscriptionPreferencesRequest) =
+ runCatching {
+ knuticeService.create(KnuticeService::class.java).submitTopicSubscriptionPreferences(request)
+ }.onFailure { throw it }
+}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/model/Requests.kt b/core/network/src/main/kotlin/com/doyoonkim/network/model/Requests.kt
new file mode 100644
index 00000000..3edcd103
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/model/Requests.kt
@@ -0,0 +1,25 @@
+package com.doyoonkim.network.model
+
+import com.doyoonkim.model.requestBody.DeviceTokenBody
+import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody
+import com.doyoonkim.model.requestBody.UserReportBody
+import model.NetworkResult
+
+/**
+ * @author kimdoyoon
+ * Created 6/3/25 at 12:10 AM
+ */
+data class DeviceTokenRequest(
+ val result: NetworkResult? = NetworkResult(),
+ val body: DeviceTokenBody
+)
+
+data class UserReportRequest(
+ val result: NetworkResult? = NetworkResult(),
+ val body: UserReportBody
+)
+
+data class TopicSubscriptionPreferencesRequest(
+ val result: NetworkResult? = NetworkResult(),
+ val body: TopicSubscriptionPreferencesBody
+)
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/model/Responses.kt b/core/network/src/main/kotlin/com/doyoonkim/network/model/Responses.kt
new file mode 100644
index 00000000..b14967d4
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/model/Responses.kt
@@ -0,0 +1,46 @@
+package model
+
+import com.doyoonkim.network.model.dto.TopicSubscriptionPreferencesDTO
+import com.google.gson.annotations.SerializedName
+import model.dto.NoticeDTO
+import model.dto.TopThreeNoticeDTO
+
+/**
+ * @author kimdoyoon
+ * Created 6/2/25 at 11:04 PM
+ */
+data class NetworkResult(
+ @SerializedName("resultCode") var resultCode: Int? = null,
+ @SerializedName("resultMessage") var resultMessage: String? = null,
+ @SerializedName("resultDescription") var resultDescription: String? = null
+)
+
+data class TopThreeNoticeResults(
+ @SerializedName("result") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: TopThreeNoticeDTO? = TopThreeNoticeDTO()
+)
+
+data class NoticesPerPageResult(
+ @SerializedName("result") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: ArrayList
+)
+
+data class NoticeByIdResult(
+ @SerializedName("result") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: NoticeDTO? = NoticeDTO()
+)
+
+data class NoticesByKeywordResult(
+ @SerializedName("result") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: ArrayList
+)
+
+data class TopicSubscriptionPreferencesResult(
+ @SerializedName("reuslt") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: TopicSubscriptionPreferencesDTO? = TopicSubscriptionPreferencesDTO()
+)
+
+data class PostResult(
+ @SerializedName("result") var result: NetworkResult? = NetworkResult(),
+ @SerializedName("body") var body: Boolean? = null
+)
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/NoticeDTO.kt b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/NoticeDTO.kt
new file mode 100644
index 00000000..59b0314e
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/NoticeDTO.kt
@@ -0,0 +1,28 @@
+package model.dto
+
+import com.doyoonkim.model.NoticeVO
+import com.google.gson.annotations.SerializedName
+
+/**
+ * @author kimdoyoon
+ * Created 6/2/25 at 11:08 PM
+ */
+data class NoticeDTO(
+ @SerializedName("nttId") var nttId: Int? = null,
+ @SerializedName("title") var title: String? = null,
+ @SerializedName("contentUrl") var contentUrl: String? = null,
+ @SerializedName("contentImage") var contentImage: String? = null,
+ @SerializedName("departmentName") var departName: String? = null,
+ @SerializedName("registeredAt") var registeredAt: String? = null,
+ @SerializedName("noticeName") var noticeCategory: String? = null
+) {
+ fun toVO() =
+ NoticeVO(
+ nttId = this.nttId ?: -1,
+ title = this.title ?: "",
+ url = this.contentUrl ?: "",
+ imageUrl = this.contentImage,
+ departName = this.departName ?: "",
+ timestamp = this.registeredAt ?: ""
+ )
+}
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopThreeNoticeDTO.kt b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopThreeNoticeDTO.kt
new file mode 100644
index 00000000..e2678d4e
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopThreeNoticeDTO.kt
@@ -0,0 +1,23 @@
+package model.dto
+
+import com.doyoonkim.model.TopThreeNoticeVO
+import com.google.gson.annotations.SerializedName
+
+/**
+ * @author kimdoyoon
+ * Created 6/2/25 at 11:01 PM
+ */
+data class TopThreeNoticeDTO(
+ @SerializedName("latestThreeGeneralNews") var generalNotices: ArrayList = arrayListOf(),
+ @SerializedName("latestThreeScholarshipNews") var scholarshipNotices: ArrayList = arrayListOf(),
+ @SerializedName("latestThreeEventNews") var eventNotices: ArrayList = arrayListOf(),
+ @SerializedName("latestThreeAcademicNews") var academicNews: ArrayList = arrayListOf()
+) {
+ fun toVO() =
+ TopThreeNoticeVO(
+ general = this.generalNotices.map { it.toVO() },
+ scholarship = this.scholarshipNotices.map { it.toVO() },
+ event = this.eventNotices.map { it.toVO() },
+ academic = this.academicNews.map { it.toVO() }
+ )
+}
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopicSubscriptionPreferencesDTO.kt b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopicSubscriptionPreferencesDTO.kt
new file mode 100644
index 00000000..3b25405b
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/model/dto/TopicSubscriptionPreferencesDTO.kt
@@ -0,0 +1,25 @@
+package com.doyoonkim.network.model.dto
+
+import com.doyoonkim.model.TopicSubscriptionPreferencesVO
+import com.google.gson.annotations.SerializedName
+
+/**
+ * @author kimdoyoon
+ * Created 6/3/25 at 12:23 AM
+ */
+data class TopicSubscriptionPreferencesDTO(
+ @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
+) {
+ fun toVO() =
+ TopicSubscriptionPreferencesVO(
+ general = this.generalNewsTopic,
+ scholarship = this.scholarshipNewsTopic,
+ event = this.eventNewsTopic,
+ academic = this.academicNewsTopic,
+ employment = this.employmentNewsTopic
+ )
+}
diff --git a/core/network/src/main/kotlin/com/doyoonkim/network/retrofit/KnuticeService.kt b/core/network/src/main/kotlin/com/doyoonkim/network/retrofit/KnuticeService.kt
new file mode 100644
index 00000000..36c5a975
--- /dev/null
+++ b/core/network/src/main/kotlin/com/doyoonkim/network/retrofit/KnuticeService.kt
@@ -0,0 +1,69 @@
+package com.doyoonkim.network.retrofit
+
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.network.model.DeviceTokenRequest
+import com.doyoonkim.network.model.TopicSubscriptionPreferencesRequest
+import com.doyoonkim.network.model.UserReportRequest
+import model.NoticeByIdResult
+import model.NoticesByKeywordResult
+import model.NoticesPerPageResult
+import model.PostResult
+import model.TopThreeNoticeResults
+import model.TopicSubscriptionPreferencesResult
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Headers
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+/**
+ * @author kimdoyoon
+ * Created 6/2/25 at 10:58 PM
+ */
+
+interface KnuticeService {
+ @GET("open-api/notice")
+ suspend fun getTopThreeNotices(): TopThreeNoticeResults
+
+ @GET("open-api/notice/list")
+ suspend fun getNoticesPerPage(
+ @Query("noticeName") category: NoticeCategory,
+ @Query("nttId") lastNttId: Int? = null
+ ): NoticesPerPageResult
+
+ @GET("open-api/notice/{nttId}")
+ suspend fun getNoticeById(
+ @Path("nttId") nttId: String
+ ): NoticeByIdResult
+
+ @GET("open-api/search")
+ suspend fun getNoticesByKeyword(
+ @Query("keyword") keyword: String
+ ): NoticesByKeywordResult
+
+ @GET("open-api/topic")
+ suspend fun getTopicSubscriptionStatus(
+ @Header("fcmToken") token: String
+ ): TopicSubscriptionPreferencesResult
+
+ @Headers("Content-Type: application/json")
+ @POST("open-api/fcm")
+ suspend fun validateToken(
+ @Body request: DeviceTokenRequest
+ ): PostResult
+
+ @Headers("Content-Type: application/json")
+ @POST("open-api/report")
+ suspend fun submitUserReport(
+ @Body request: UserReportRequest
+ ): PostResult
+
+ @Headers("Content-Type: application/json")
+ @POST("open-api/topic")
+ suspend fun submitTopicSubscriptionPreferences(
+ @Body request: TopicSubscriptionPreferencesRequest
+ ): PostResult
+
+}
\ No newline at end of file
diff --git a/core/network/src/test/java/com/doyoonkim/network/ExampleUnitTest.kt b/core/network/src/test/java/com/doyoonkim/network/ExampleUnitTest.kt
new file mode 100644
index 00000000..eef945a5
--- /dev/null
+++ b/core/network/src/test/java/com/doyoonkim/network/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.network
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/core/notification/.gitignore b/core/notification/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/notification/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/notification/build.gradle.kts b/core/notification/build.gradle.kts
new file mode 100644
index 00000000..0ad1f7da
--- /dev/null
+++ b/core/notification/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+
+ // For Dagger
+ id("kotlin-kapt")
+}
+
+android {
+ namespace = "com.doyoonkim.notification"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 31
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(projects.core.domain)
+ implementation(projects.common)
+ implementation(projects.core.model)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
+ implementation(libs.firebase.messaging)
+ implementation(libs.firebase.messaging.directboot)
+}
\ No newline at end of file
diff --git a/core/notification/consumer-rules.pro b/core/notification/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/notification/proguard-rules.pro b/core/notification/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/notification/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/core/notification/src/androidTest/java/com/doyoonkim/notification/ExampleInstrumentedTest.kt b/core/notification/src/androidTest/java/com/doyoonkim/notification/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..beb1dc31
--- /dev/null
+++ b/core/notification/src/androidTest/java/com/doyoonkim/notification/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.notification
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.notification.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/notification/src/main/AndroidManifest.xml b/core/notification/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/core/notification/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/di/NotificationModule.kt b/core/notification/src/main/java/com/doyoonkim/notification/di/NotificationModule.kt
new file mode 100644
index 00000000..59fd848a
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/di/NotificationModule.kt
@@ -0,0 +1,29 @@
+package com.doyoonkim.notification.di
+
+import android.content.Context
+import com.doyoonkim.common.BitmapHandler
+import com.doyoonkim.common.di.ApplicationContext
+import com.doyoonkim.domain.ImageRepository
+import com.doyoonkim.domain.usecases.ValidateDeviceToken
+import com.doyoonkim.notification.fcm.PushNotificationHandler
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+object NotificationModule {
+
+ @Provides
+ @Singleton
+ fun providesPushNotificationHandler(
+ imageRepository: ImageRepository,
+ bitmapHandler: BitmapHandler,
+ @ApplicationContext context: Context
+ ) =
+ PushNotificationHandler(
+ imageRepository,
+ bitmapHandler,
+ context
+ )
+
+}
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationHandler.kt b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationHandler.kt
new file mode 100644
index 00000000..f1770ecc
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationHandler.kt
@@ -0,0 +1,149 @@
+package com.doyoonkim.notification.fcm
+
+import android.Manifest
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.net.toUri
+import com.doyoonkim.common.BitmapHandler
+import com.doyoonkim.common.R
+import com.doyoonkim.domain.ImageRepository
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import javax.inject.Inject
+import kotlin.random.Random
+
+
+class PushNotificationHandler @Inject constructor(
+ private val imageRepository: ImageRepository,
+ private val bitMapHandler: BitmapHandler,
+ private val context: Context
+) {
+ private val TAG = "PushNotificationHandler"
+
+ @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
+ fun handleReceivedMessage(message: RemoteMessage) {
+ // 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}") // message.data: Map
+
+ message.toPushNotification()
+
+
+ // Apply "Do not disturb" option. (Temporarily save the message and deliver after the core time is end.
+ // Use Local Database (Room?)
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
+ private fun RemoteMessage.toPushNotification() {
+
+ // Create Pending Intent (For access push notification while the app is in foreground)
+ // TODO: Migrate to Deep Link.
+ val nttId = this@toPushNotification.data["nttId"]
+ val url = this@toPushNotification.data["contentUrl"]
+ val imageUrl = this@toPushNotification.data["contentImage"]
+ val fabVisible = true
+
+ // Deeplink featured by Jetpack Navigation won't work because notification payload consumes custom-defined deeplink intent using ACTION_VIEW
+ val deeplinkIntent = Intent(
+ Intent.ACTION_VIEW,
+ "knutice://service/noticeDetail/$nttId/${Uri.encode(url)}/$fabVisible".toUri()
+ ).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ deeplinkIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notificationId = Random(System.currentTimeMillis().toInt()).nextInt()
+ // Utilize channel already created by FCM as default
+ val notificationBuilder = NotificationCompat.Builder(
+ context, context.getString(R.string.inapp_notification_channel_id)
+ ).apply {
+ setSmallIcon(R.mipmap.ic_launcher)
+ setLargeIcon(Icon.createWithResource(context, R.mipmap.ic_launcher))
+ setContentTitle(context.getString(R.string.new_notice))
+ setContentText(this@toPushNotification.data["contentTitle"] ?: "No message body.")
+ setContentIntent(pendingIntent)
+ setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ setAutoCancel(true)
+ }
+
+
+ with(NotificationManagerCompat.from(context)) {
+ if (ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ Log.d("NotificationHandler", "Permission Denied")
+ // TODO: Consider calling
+ // ActivityCompat#requestPermissions
+ // here to request the missing permissions, and then overriding
+ // public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ // int[] grantResults)
+ // to handle the case where the user grants the permission. See the documentation
+ // for ActivityCompat#requestPermissions for more details.
+ return
+ }
+
+ // HardCoded CoroutineScope for Testing
+ val completionMarker = Job() // Variable for manual Cancellation
+ imageUrl?.let { url ->
+ CoroutineScope(Dispatchers.IO + completionMarker).launch {
+ val bitmapImage = async {
+ runCatching {
+ withTimeout(5000L) {
+ imageRepository.getImageByteArrayFromUrl(url)?.let { b ->
+ bitMapHandler.decodeByteArray(b)
+ }
+ }
+ }.onFailure {
+ Log.d(TAG, "Unable to retrieve bitmap image\nREASON: ${it.stackTrace}")
+ }
+ }
+
+ bitmapImage.await().fold(
+ onSuccess = { result ->
+ result?.let {
+ notificationBuilder.apply {
+ setStyle(
+ NotificationCompat.BigPictureStyle()
+ .bigPicture(it)
+ )
+ }
+ }
+ },
+ onFailure = {
+ Log.d(TAG, "No Bitmap Image to be added\nREASON: ${it.stackTrace}")
+ }
+ ).also {
+ notify(notificationId, notificationBuilder.build())
+ completionMarker.complete()
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationService.kt b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationService.kt
new file mode 100644
index 00000000..5fcc5059
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationService.kt
@@ -0,0 +1,52 @@
+package com.doyoonkim.notification.fcm
+
+import android.Manifest
+import android.content.Intent
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import com.doyoonkim.common.di.AppInjectorProvider
+import com.google.firebase.messaging.Constants.MessageNotificationKeys
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import dagger.Lazy
+import javax.inject.Inject
+
+class PushNotificationService : FirebaseMessagingService() {
+ @Inject lateinit var handlerProvider: Lazy
+ private val TAG = "PushNotificationHandler"
+
+ override fun onCreate() {
+ super.onCreate()
+ // For Field Injection
+ (applicationContext as AppInjectorProvider).appInjector.inject(this)
+ Log.d(TAG, "Initialized?: ${::handlerProvider.isInitialized}")
+ Log.d(TAG, "Called")
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ // POST request to send FCM Token to the Server.
+ Log.d(TAG, "Received Token: ${token.toString()}")
+// requestCurrentToken()
+ }
+
+ // Fix problem: onMessageReceived is not being called when app is in background/cold-start
+ override fun handleIntent(intent: Intent?) {
+ // Manually remove notification payload (https://medium.com/@jms8732/background에서-onmessagereceived가-호출-안되는-현상에-관하여-7595df624d91)
+ val newIntent = intent?.apply {
+ val temp = extras?.apply {
+ remove(MessageNotificationKeys.ENABLE_NOTIFICATION)
+ remove("gcm.notification.e")
+ }
+ replaceExtras(temp)
+ }
+ super.handleIntent(newIntent)
+ }
+
+ @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
+ override fun onMessageReceived(message: RemoteMessage) {
+ (applicationContext as AppInjectorProvider).appInjector.inject(this)
+ super.onMessageReceived(message)
+ handlerProvider.get().handleReceivedMessage(message)
+ }
+}
\ 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
new file mode 100644
index 00000000..7d3f119b
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/TokenHandler.kt
@@ -0,0 +1,42 @@
+package com.doyoonkim.notification.fcm
+
+import android.util.Log
+import com.doyoonkim.domain.usecases.ValidateDeviceToken
+import com.doyoonkim.model.requestBody.DeviceTokenBody
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.firebase.messaging.FirebaseMessaging
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class TokenHandler @Inject constructor(
+ private val validateDeviceToken: ValidateDeviceToken
+) {
+ 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
+ }
+
+ // 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")
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/local/AlarmReceiver.kt b/core/notification/src/main/java/com/doyoonkim/notification/local/AlarmReceiver.kt
new file mode 100644
index 00000000..c2f4702d
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/local/AlarmReceiver.kt
@@ -0,0 +1,20 @@
+package com.doyoonkim.notification.local
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+class AlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ context?.let {
+ val notificationManager = it.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ RunnerNotifier(notificationManager, context).run {
+ showNotification(
+ content = intent?.getStringExtra("content") ?: "",
+ uriString = intent?.getStringExtra("uri_string") ?: ""
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/local/NotificationAlarmScheduler.kt b/core/notification/src/main/java/com/doyoonkim/notification/local/NotificationAlarmScheduler.kt
new file mode 100644
index 00000000..933a0eb3
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/local/NotificationAlarmScheduler.kt
@@ -0,0 +1,81 @@
+package com.doyoonkim.notification.local
+
+import android.Manifest
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import com.doyoonkim.common.di.ApplicationContext
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.model.BookmarkVO
+import javax.inject.Inject
+
+/**
+ * Local Push Notification (Bookmark Reminder)
+ * Reference: https://medium.com/@tolgapirim25/send-notifications-at-a-specific-time-with-alarm-manager-on-android-13c7cc9d8e7a
+ */
+interface AlarmScheduler {
+ fun createPendingIntent(target: BookmarkVO, nav: BookmarkInfo): PendingIntent
+
+ fun schedule(target: BookmarkVO, nav: BookmarkInfo)
+
+ fun cancel(target: BookmarkVO, nav: BookmarkInfo)
+}
+
+class NotificationAlarmScheduler @Inject constructor(
+ @ApplicationContext private val context: Context
+) : AlarmScheduler {
+ private val TAG = "NotificationAlarmScheduler"
+ // AlarmManager Instance
+ // Context: ApplicationContext
+ private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ override fun createPendingIntent(target: BookmarkVO, nav: BookmarkInfo): PendingIntent {
+ val uri = "knutice://service/bookmark/${nav.noticeId}/${nav.noticeTitle}/${nav.noticeInfo}"
+ val intent = Intent(context, AlarmReceiver::class.java)
+ .apply {
+ putExtra("content", target.bookmarkNote)
+ putExtra("uri_string", uri)
+ }
+
+ return PendingIntent.getBroadcast(
+ context,
+ target.targetNoticeNttId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ // Permission Declared and granted at the APP level entry point.
+ @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)
+ override fun schedule(target: BookmarkVO, nav: BookmarkInfo) {
+ // Android Version Check
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (!alarmManager.canScheduleExactAlarms()) {
+ Log.d(TAG, "Schedule Exact Alarms is not possible.")
+ return
+ }
+ }
+
+ Log.d(TAG, "Alarm is being scheduled.")
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP,
+ target.reminderSchedule,
+ createPendingIntent(target, nav)
+ ).also { Log.d(TAG, "Alarm Scheduled.") }
+ }
+
+ override fun cancel(target: BookmarkVO, nav: BookmarkInfo) {
+ alarmManager.cancel(
+ createPendingIntent(target, nav)
+ )
+ }
+
+ fun canScheduleExactAlarms(): Boolean {
+ return alarmManager.canScheduleExactAlarms()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/alarm/Notifier.kt b/core/notification/src/main/java/com/doyoonkim/notification/local/Notifier.kt
similarity index 50%
rename from app/src/main/java/com/doyoonkim/knutice/alarm/Notifier.kt
rename to core/notification/src/main/java/com/doyoonkim/notification/local/Notifier.kt
index 4e687ee9..85d3ebe8 100644
--- a/app/src/main/java/com/doyoonkim/knutice/alarm/Notifier.kt
+++ b/core/notification/src/main/java/com/doyoonkim/notification/local/Notifier.kt
@@ -1,25 +1,23 @@
-package com.doyoonkim.knutice.alarm
+package com.doyoonkim.notification.local
import android.app.Notification
import android.app.NotificationManager
-import android.os.Build
-import com.doyoonkim.knutice.model.Bookmark
-abstract class Notifier (
+abstract class Notifier(
private val notificationManager: NotificationManager
) {
abstract val channelId: String
abstract val channelName: String
abstract val notificationId: Int
- fun showNotification(note: String) {
- // Use existing notification channel
- val notification = buildNotification(note)
+ fun showNotification(content: String, uriString: String) {
+ // Use existing notification channel: KNUTICE-IN-APP-Channel
+ val notification = buildNotification(content, uriString)
notificationManager.notify(
notificationId,
notification
)
}
- abstract fun buildNotification(note: String): Notification
+ abstract fun buildNotification(content: String, uriString: String): Notification
}
\ No newline at end of file
diff --git a/core/notification/src/main/java/com/doyoonkim/notification/local/RunnerNotifier.kt b/core/notification/src/main/java/com/doyoonkim/notification/local/RunnerNotifier.kt
new file mode 100644
index 00000000..3463398f
--- /dev/null
+++ b/core/notification/src/main/java/com/doyoonkim/notification/local/RunnerNotifier.kt
@@ -0,0 +1,48 @@
+package com.doyoonkim.notification.local
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
+import com.doyoonkim.common.R
+import kotlin.random.Random
+
+class RunnerNotifier(
+ private val notificationManager: NotificationManager,
+ private val context: Context
+) : Notifier(notificationManager) {
+ override val channelId: String
+ get() = context.getString(R.string.inapp_notification_channel_id)
+ override val channelName: String
+ get() = context.getString(R.string.inapp_notificaiton_channel_name)
+ override val notificationId: Int
+ get() = Random(System.currentTimeMillis()).nextInt()
+
+ override fun buildNotification(content: String, uriString: String): Notification {
+ // Create Intent
+ val deeplinkIntent = Intent(
+ Intent.ACTION_VIEW,
+ uriString.toUri()
+ ).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }.run {
+ PendingIntent.getActivity(
+ context,
+ 0,
+ this,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ // Custom-define notification builder.
+ return NotificationCompat.Builder(context, channelId)
+ .setContentTitle(context.getString(R.string.text_reminder_title))
+ .setContentText(content)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentIntent(deeplinkIntent)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/core/notification/src/test/java/com/doyoonkim/notification/ExampleUnitTest.kt b/core/notification/src/test/java/com/doyoonkim/notification/ExampleUnitTest.kt
new file mode 100644
index 00000000..c8208c0e
--- /dev/null
+++ b/core/notification/src/test/java/com/doyoonkim/notification/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.notification
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/feature/bookmark/.gitignore b/feature/bookmark/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/bookmark/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/bookmark/build.gradle.kts b/feature/bookmark/build.gradle.kts
new file mode 100644
index 00000000..a77bd111
--- /dev/null
+++ b/feature/bookmark/build.gradle.kts
@@ -0,0 +1,78 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ id("kotlin-kapt")
+
+ // Required from Kotlin 2.0.0 (Every module using Compose)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.doyoonkim.bookmark"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 30
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(projects.common)
+ implementation(projects.core.domain)
+ implementation(projects.core.model)
+ implementation(projects.core.notification)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+
+ // Universally applied to module uses UI feature
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material)
+
+
+ testImplementation(libs.junit)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
+ implementation(libs.androidx.navigation.compose)
+}
\ No newline at end of file
diff --git a/feature/bookmark/consumer-rules.pro b/feature/bookmark/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/bookmark/proguard-rules.pro b/feature/bookmark/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/bookmark/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/bookmark/src/androidTest/java/com/doyoonkim/bookmark/ExampleInstrumentedTest.kt b/feature/bookmark/src/androidTest/java/com/doyoonkim/bookmark/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..d91443f7
--- /dev/null
+++ b/feature/bookmark/src/androidTest/java/com/doyoonkim/bookmark/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.bookmark
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.bookmark.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/feature/bookmark/src/main/AndroidManifest.xml b/feature/bookmark/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/bookmark/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ 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
new file mode 100644
index 00000000..8ddbfcbd
--- /dev/null
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/bookmarkServiceGraph.kt
@@ -0,0 +1,69 @@
+package com.doyoonkim.bookmark
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
+import com.doyoonkim.bookmark.edit.EditBookmarkScreen
+import com.doyoonkim.bookmark.list.BookmarkListScreen
+import com.doyoonkim.bookmark.viewmodel.BookmarkListViewModel
+import com.doyoonkim.bookmark.viewmodel.EditBookmarkViewModel
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.common.navigation.NavRoutes
+import com.doyoonkim.common.navigation.NoticeDetail
+
+fun NavGraphBuilder.bookmarkServiceGraph(
+ navController: NavController,
+ viewModelFactory: ViewModelProvider.Factory,
+ contentPadding: PaddingValues,
+ onNoticeDetailRequested: (NoticeDetail) -> Unit
+) {
+
+ composable(NavRoutes.Bookmark.route) {
+ BookmarkListScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ bottomPadding = contentPadding.calculateBottomPadding(),
+ onBookmarkSelected = {
+ navController.navigate("bookmark/${it.noticeId}/${it.noticeTitle}/${it.noticeInfo}")
+ },
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+
+ composable(
+ route = "bookmark/{id}/{title}/{info}",
+ deepLinks = listOf(
+ navDeepLink {
+ uriPattern = "knutice://service/bookmark/{id}/{title}/{info}"
+ }
+ )
+ ) { backStackEntry ->
+ val bookmarkInfo = backStackEntry.arguments?.let {
+ BookmarkInfo(
+ it.getString("id")?.toInt() ?: 0,
+ it.getString("title") ?: "",
+ it.getString("info") ?: ""
+ )
+ } ?: BookmarkInfo(0, "", "")
+
+ EditBookmarkScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ bookmarkInfo = bookmarkInfo,
+ onNoticeSelected = { onNoticeDetailRequested(it) },
+ onBackPressed = { navController.navigate(NavRoutes.Bookmark.route) {
+ popUpTo(navController.graph.startDestinationId) {
+ inclusive = true
+ }
+ } }
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/di/BookmarkModule.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/di/BookmarkModule.kt
new file mode 100644
index 00000000..c6432eb9
--- /dev/null
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/di/BookmarkModule.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.bookmark.di
+
+import androidx.lifecycle.ViewModel
+import com.doyoonkim.bookmark.viewmodel.BookmarkListViewModel
+import com.doyoonkim.bookmark.viewmodel.EditBookmarkViewModel
+import com.doyoonkim.common.di.ViewModelKey
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class BookmarkModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(BookmarkListViewModel::class)
+ abstract fun bindsBookmarkListViewModel(viewModel: BookmarkListViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(EditBookmarkViewModel::class)
+ abstract fun bindsEditBookmarkViewModel(viewModel: EditBookmarkViewModel): ViewModel
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/edit/EditBookmarkScreen.kt
similarity index 70%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt
rename to feature/bookmark/src/main/java/com/doyoonkim/bookmark/edit/EditBookmarkScreen.kt
index 71807e57..57e356da 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/edit/EditBookmarkScreen.kt
@@ -1,6 +1,7 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.bookmark.edit
import android.util.Log
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
@@ -14,36 +15,36 @@ 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.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AlertDialogDefaults
+import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@@ -51,23 +52,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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
-import com.doyoonkim.knutice.ui.theme.subTitle
-import com.doyoonkim.knutice.ui.theme.title
-import com.doyoonkim.knutice.viewModel.EditBookmarkViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.doyoonkim.bookmark.viewmodel.EditBookmarkViewModel
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.common.navigation.NoticeDetail
+import com.doyoonkim.common.ui.DateTimePicker
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.buttonPurple
+import com.doyoonkim.common.theme.containerBackground
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
+import com.doyoonkim.common.ui.NotificationPreviewCard
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
@@ -76,32 +76,48 @@ import java.util.Date
import java.util.Locale
import java.util.TimeZone
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun EditBookmark(
+fun EditBookmarkScreen(
modifier: Modifier = Modifier,
- viewModel: EditBookmarkViewModel = hiltViewModel(),
- onDetailedNoticeRequested: (Notice) -> Unit,
- onSaveClicked: (Bookmark?) -> Unit = { }
+ viewModel: EditBookmarkViewModel,
+ bookmarkInfo: BookmarkInfo,
+ onNoticeSelected: (NoticeDetail) -> Unit,
+ onBackPressed: () -> Unit
) {
- val uiState by viewModel.uiState.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val adjustImePadding = Modifier.consumeWindowInsets(WindowInsets.ime).imePadding()
- LaunchedEffect(uiState.isReminderRequested) {
- if (uiState.timeForRemind == 0L) {
- viewModel.updateReminderOptions(updatedTimeForRemind = System.currentTimeMillis())
+ BackHandler {
+ onBackPressed()
+ }
+
+ // Once per composition?
+ LaunchedEffect(Unit) {
+ viewModel.apply {
+ createBookmarkInfo(bookmarkInfo)
+ getBookmarkByNoticeId(bookmarkInfo.noticeId)
+ getNoticeById(bookmarkInfo.noticeId)
}
}
+ LaunchedEffect(uiState.isReminderRequested) {
+ if (uiState.timeForRemind == 0L)
+ viewModel.updateReminderOptions(timeForRemind = System.currentTimeMillis())
+ }
+
Column(
modifier = modifier.fillMaxSize()
+ .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom))
) {
NotificationPreviewCard(
modifier = Modifier.padding(5.dp),
- notificationTitle = uiState.targetNotice.title,
- notificationInfo = uiState.targetNotice.departName
+ isLoading = false,
+ notificationTitle = bookmarkInfo.noticeTitle,
+ notificationInfo = bookmarkInfo.noticeInfo
) {
// Request Full Content
- onDetailedNoticeRequested(uiState.targetNotice)
+ uiState.targetNotice?.let { onNoticeSelected(NoticeDetail(it.nttId, it.url, false)) }
}
Spacer(Modifier.height(30.dp))
@@ -142,7 +158,7 @@ fun EditBookmark(
checked = uiState.isReminderRequested,
enabled = true,
modifier = Modifier.padding(10.dp).weight(1f),
- onCheckedChange = { viewModel.updateReminderOptions(reminderRequested = !uiState.isReminderRequested) }
+ onCheckedChange = { viewModel.updateReminderOptions(requested = !uiState.isReminderRequested) }
)
}
AnimatedVisibility(
@@ -162,10 +178,11 @@ fun EditBookmark(
Text(
modifier = Modifier.fillMaxWidth().padding(20.dp)
.clickable { viewModel.updateReminderOptions(
- updatedDatePickerVisible = !uiState.datePickerVisible
+ isDatePickerVisible = !uiState.datePickerVisible
) },
text = uiState.timeForRemind.toFormattedDate(
- SimpleDateFormat("yyyy/MM/dd a HH:mm", Locale.getDefault())),
+ SimpleDateFormat("yyyy/MM/dd a HH:mm", Locale.getDefault())
+ ),
textAlign = TextAlign.Start,
fontWeight = FontWeight.Normal,
fontSize = 15.sp,
@@ -232,16 +249,16 @@ fun EditBookmark(
modifier = Modifier.wrapContentHeight()
.weight(1f),
enabled = true,
+ colors = ButtonDefaults.buttonColors().copy(
+ containerColor = MaterialTheme.colorScheme.buttonPurple,
+ contentColor = Color.White,
+ ),
shape = RoundedCornerShape(10.dp),
onClick = {
- if (!uiState.requireCreation) {
- viewModel.modifyBookmark()
- } else {
- viewModel.createNewBookmark()
- }
+ viewModel.submitBookmark()
coroutineScope.launch {
delay(500L)
- onSaveClicked(uiState.bookmarkInstance)
+// onSaveClicked(uiState.bookmarkInstance)
}
}
) {
@@ -254,13 +271,17 @@ fun EditBookmark(
}
if (!uiState.requireCreation) {
- OutlinedButton(
+ Button(
modifier = Modifier.wrapContentHeight().weight(1f),
enabled = true,
shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors().copy(
+ containerColor = MaterialTheme.colorScheme.subTitle,
+ contentColor = Color.White
+ ),
onClick = {
viewModel.removeBookmark()
- onSaveClicked(null)
+// onSaveClicked(null)
}
) {
Text(
@@ -284,7 +305,7 @@ fun EditBookmark(
Box(
modifier = Modifier.fillMaxSize().padding(start = 5.dp, end = 5.dp)
.clickable { viewModel.updateReminderOptions(
- updatedDatePickerVisible = !uiState.datePickerVisible)
+ isDatePickerVisible = !uiState.datePickerVisible)
}
) {
DateTimePicker(
@@ -295,25 +316,52 @@ fun EditBookmark(
if (it != null) {
Log.d("EditBookmark", "${it}")
viewModel.updateReminderOptions(
- updatedTimeForRemind = it,
- updatedDatePickerVisible = !uiState.datePickerVisible
+ timeForRemind = it,
+ isDatePickerVisible = !uiState.datePickerVisible
)
}
}
}
+ }
+
+ if (uiState.isCompleted) {
+ BasicAlertDialog(
+ onDismissRequest = {
+ // Dismiss the dialog when the user clicks outside the dialog or on the back
+ // button. If you want to disable that functionality, simply use an empty
+ // onDismissRequest.
+ }
+ ) {
+ Surface(
+ modifier = Modifier.wrapContentWidth().wrapContentHeight(),
+ shape = MaterialTheme.shapes.large,
+ tonalElevation = AlertDialogDefaults.TonalElevation
+ ) {
+ Column(modifier = Modifier.padding(30.dp)) {
+ Text(
+ text = if (uiState.isSuccessful) {
+ stringResource(R.string.text_save_successful)
+ } else {
+ stringResource(R.string.text_save_unsuccessful)
+ }
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ TextButton(
+ onClick = {
+ viewModel.updateCompletionStatus(false)
+ if (uiState.isSuccessful) onBackPressed()
+ },
+ modifier = Modifier.align(Alignment.End)
+ ) {
+ Text(stringResource(R.string.btn_confirm))
+ }
+ }
+ }
+ }
}
}
private fun Long.toFormattedDate(f: SimpleDateFormat): String {
return f.apply { timeZone = TimeZone.getTimeZone(ZoneId.systemDefault()) }.format(Date(this))
-}
-
-
-
-@Preview(showBackground = true)
-@Composable
-fun EditBookmark_Preview() {
-// EditBookmark(Modifier.fillMaxSize().padding(10.dp)) { }
-}
-
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..e3884eef
--- /dev/null
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt
@@ -0,0 +1,106 @@
+package com.doyoonkim.bookmark.list
+
+import android.util.Log
+import androidx.activity.compose.BackHandler
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.doyoonkim.bookmark.viewmodel.BookmarkListViewModel
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.common.theme.containerBackgroundSolid
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.ui.NotificationPreviewCardMarked
+
+@Composable
+fun BookmarkListScreen(
+ modifier: Modifier = Modifier,
+ viewModel: BookmarkListViewModel,
+ bottomPadding: Dp = 0.dp,
+ onBookmarkSelected: (BookmarkInfo) -> Unit,
+ onBackPressed: () -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ BackHandler { onBackPressed() }
+
+ LaunchedEffect(Unit) {
+ viewModel.getAllBookmarks()
+ }
+
+ Box(
+ modifier = modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.containerBackgroundSolid)
+ .systemBarsPadding()
+ ) {
+ if (uiState.bookmarks.isEmpty()) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.text_no_bookmark),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.subTitle
+ )
+ Spacer(Modifier.height(bottomPadding))
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.wrapContentHeight()
+ .fillMaxWidth()
+ .padding(top = 12.dp, bottom = bottomPadding)
+ .background(Color.Transparent),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(
+ items = uiState.bookmarks,
+ key = { it.second.nttId }
+ ) {
+ // Being called 3 times
+ Log.d("BookmarkComposable", "Element: $it")
+ NotificationPreviewCardMarked(
+ noticeTitle = it.second.title,
+ noticeSubtitle = "[${it.second.departName}] ${it.second.timestamp}",
+ onItemClicked = { onBookmarkSelected(
+ it.second.run {
+ BookmarkInfo(
+ noticeId = this.nttId,
+ noticeTitle = this.title,
+ noticeInfo = "[${this.departName}] ${this.timestamp}"
+ )
+ }
+ ) }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..fb17f337
--- /dev/null
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt
@@ -0,0 +1,57 @@
+package com.doyoonkim.bookmark.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchAllBookmarks
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class BookmarkListViewModel @Inject constructor(
+ private val fetchAllBookmarks: FetchAllBookmarks
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(BookmarkListState())
+ val uiState = _uiState.asStateFlow()
+
+ fun getAllBookmarks() {
+ updateFetchingStatus(false)
+ viewModelScope.launch {
+ fetchAllBookmarks()
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ Log.d("BookmarkListViewModel", "${result}")
+ _uiState.update {
+ it.copy(
+ bookmarks = it.bookmarks.toMutableList().apply {
+ this.add(result)
+ }.distinctBy { e -> e.first.bookmarkId }.toList()
+ )
+ }
+ }
+ }.run { if (this.isCompleted) updateFetchingStatus(true) }
+ }
+
+ fun updateFetchingStatus(status: Boolean) =
+ _uiState.update {
+ it.copy(
+ isFetchingCompleted = status
+ )
+ }
+
+
+}
+
+data class BookmarkListState(
+ val bookmarks: List> = emptyList(),
+ val isRefreshing: Boolean = false,
+ val isFetchingCompleted: Boolean = true,
+)
\ No newline at end of file
diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/EditBookmarkViewModel.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/EditBookmarkViewModel.kt
new file mode 100644
index 00000000..8d5ccae4
--- /dev/null
+++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/EditBookmarkViewModel.kt
@@ -0,0 +1,206 @@
+package com.doyoonkim.bookmark.viewmodel
+
+import androidx.annotation.RequiresPermission
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.domain.usecases.FetchNoticeByIdFromLocal
+import com.doyoonkim.domain.usecases.ModifyBookmark
+import com.doyoonkim.model.BookmarkVO
+import com.doyoonkim.model.NoticeVO
+import com.doyoonkim.notification.local.NotificationAlarmScheduler
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class EditBookmarkViewModel @Inject constructor(
+ private val modifyBookmark: ModifyBookmark,
+ private val fetchNoticeByIdLocal: FetchNoticeByIdFromLocal,
+ private val alarmScheduler: NotificationAlarmScheduler
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(EditBookmarkState())
+ val uiState = _uiState.asStateFlow()
+
+ private var bookmarkNav: BookmarkInfo? = null
+
+ fun getBookmarkByNoticeId(nttId: Int) =
+ viewModelScope.launch {
+ modifyBookmark.query(nttId)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ _uiState.update {
+ it.copy(
+ bookmarkId = result.bookmarkId,
+ isReminderRequested = result.isScheduled,
+ timeForRemind = result.reminderSchedule,
+ bookmarkNote = result.bookmarkNote,
+ requireCreation = false,
+ bookmarkInstances = result
+ )
+ }
+ }
+ }
+
+ fun getNoticeById(nttId: Int) {
+ _uiState.update {
+ it.copy(
+ targetNoticeId = nttId
+ )
+ }
+
+ viewModelScope.launch {
+ fetchNoticeByIdLocal(nttId)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { notice ->
+ _uiState.update {
+ it.copy(
+ targetNotice = notice
+ )
+ }
+ }
+ }
+ }
+
+ fun createBookmarkInfo(info: BookmarkInfo) {
+ bookmarkNav = info
+ }
+
+ fun updateCompletionStatus(status: Boolean) =
+ _uiState.update {
+ it.copy(
+ isCompleted = status
+ )
+ }
+
+ fun updateReminderOptions(
+ requested: Boolean = uiState.value.isReminderRequested,
+ timeForRemind: Long = uiState.value.timeForRemind,
+ isDatePickerVisible: Boolean = uiState.value.datePickerVisible
+ ) {
+ _uiState.update {
+ it.copy(
+ isReminderRequested = requested,
+ timeForRemind = timeForRemind,
+ datePickerVisible = isDatePickerVisible
+ )
+ }
+ }
+
+ fun updateBookmarkNotes(notes: String) {
+ if (notes.length < 500) {
+ _uiState.update {
+ it.copy(
+ bookmarkNote = notes
+ )
+ }
+ }
+ }
+
+ @RequiresPermission("android.permission.SCHEDULE_EXACT_ALARM")
+ fun submitBookmark() =
+ viewModelScope.launch {
+ // Bookmark creation requires getting Notice Instance.
+ val bookmark = uiState.value.run {
+ if (this.targetNotice != null) {
+ // Update
+ BookmarkVO(
+ bookmarkId = bookmarkId,
+ targetNoticeNttId = targetNoticeId,
+ isScheduled = isReminderRequested,
+ reminderSchedule = timeForRemind,
+ bookmarkNote = bookmarkNote
+ )
+ } else {
+ // Creation
+ BookmarkVO(
+ targetNoticeNttId = targetNoticeId,
+ isScheduled = isReminderRequested,
+ reminderSchedule = timeForRemind,
+ bookmarkNote = bookmarkNote
+ )
+ }
+ }
+
+ modifyBookmark.createOrUpdate(bookmark, uiState.value.targetNotice)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ if (result) {
+ // Access AlarmScheduler to set local alarm
+ bookmarkNav?.let {
+ if (bookmark.isScheduled) alarmScheduler.schedule(bookmark, it)
+ else alarmScheduler.cancel(bookmark, it)
+ }
+ }
+ _uiState.update {
+ it.copy(
+ isSuccessful = result,
+ isCompleted = true
+ )
+ }
+ }
+ }
+
+ @RequiresPermission("android.permission.SCHEDULE_EXACT_ALARM")
+ fun removeBookmark() {
+ viewModelScope.launch {
+ val bookmark = uiState.value.run {
+ BookmarkVO(
+ bookmarkId = bookmarkId,
+ targetNoticeNttId = targetNoticeId,
+ isScheduled = isReminderRequested,
+ reminderSchedule = timeForRemind,
+ bookmarkNote = bookmarkNote
+ )
+ }
+
+ if (uiState.value.targetNotice == null) {
+ _uiState.update {
+ it.copy(
+ isSuccessful = false,
+ isCompleted = true
+ )
+ }
+ } else {
+ modifyBookmark.delete(bookmark, uiState.value.targetNotice!!)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ if (result) {
+ // Access AlarmScheduler to set local alarm
+ bookmarkNav?.let {
+ if (bookmark.isScheduled) alarmScheduler.schedule(bookmark, it)
+ else alarmScheduler.cancel(bookmark, it)
+ }
+ }
+ _uiState.update {
+ it.copy(
+ requireCreation = true,
+ isSuccessful = true,
+ isCompleted = true
+ )
+ }
+ }
+ }
+ }
+ }
+
+}
+
+data class EditBookmarkState(
+ val bookmarkId: Int = 0,
+ val targetNoticeId: Int = 0,
+ val isReminderRequested: Boolean = false,
+ val timeForRemind: Long = 0,
+ val bookmarkNote: String = "",
+ val requireCreation: Boolean = true,
+ val bookmarkInstances: BookmarkVO? = null,
+ val targetNotice: NoticeVO? = null,
+ val datePickerVisible: Boolean = false,
+ val isSuccessful: Boolean = false,
+ val isCompleted: Boolean = false
+)
\ No newline at end of file
diff --git a/feature/bookmark/src/test/java/com/doyoonkim/bookmark/ExampleUnitTest.kt b/feature/bookmark/src/test/java/com/doyoonkim/bookmark/ExampleUnitTest.kt
new file mode 100644
index 00000000..920620c0
--- /dev/null
+++ b/feature/bookmark/src/test/java/com/doyoonkim/bookmark/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.bookmark
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/feature/main/.gitignore b/feature/main/.gitignore
new file mode 100644
index 00000000..dd650708
--- /dev/null
+++ b/feature/main/.gitignore
@@ -0,0 +1,4 @@
+/build
+
+# local properties
+local.properties
\ No newline at end of file
diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts
new file mode 100644
index 00000000..eb9b31f5
--- /dev/null
+++ b/feature/main/build.gradle.kts
@@ -0,0 +1,74 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ id("kotlin-kapt")
+
+ // Required from Kotlin 2.0.0 (Every module using Compose)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.doyoonkim.main"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 30
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(projects.core.model)
+ implementation(projects.core.domain)
+ implementation(projects.common)
+
+ // Universally applied to module uses UI feature
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material)
+
+
+ testImplementation(libs.junit)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ // Dagger
+ implementation(libs.dagger)
+ implementation(libs.dagger.android)
+ implementation(libs.dagger.android.support)
+ kapt(libs.dagger.compiler)
+ kapt(libs.dagger.android.processor)
+
+ // Navigation for Compose
+ implementation(libs.androidx.navigation.compose)
+}
\ No newline at end of file
diff --git a/feature/main/consumer-rules.pro b/feature/main/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/main/proguard-rules.pro b/feature/main/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/main/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/main/src/androidTest/java/com/doyoonkim/main/ExampleInstrumentedTest.kt b/feature/main/src/androidTest/java/com/doyoonkim/main/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..871571e9
--- /dev/null
+++ b/feature/main/src/androidTest/java/com/doyoonkim/main/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.main
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.doyoonkim.main.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/main/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt b/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt
new file mode 100644
index 00000000..5b5decb6
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/MainServiceNavGraph.kt
@@ -0,0 +1,188 @@
+package com.doyoonkim.main
+
+import android.net.Uri
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
+import com.doyoonkim.common.navigation.BookmarkInfo
+import com.doyoonkim.common.navigation.Destination
+import com.doyoonkim.common.navigation.NavRoutes
+import com.doyoonkim.common.navigation.NoticeDetail
+import com.doyoonkim.main.home.HomeScreen
+import com.doyoonkim.main.notice.NoticeDetailScreen
+import com.doyoonkim.main.notice.NoticeSearchScreen
+import com.doyoonkim.main.notice.NoticesInCategoryScreen
+import com.doyoonkim.main.preference.CustomerServiceScreen
+import com.doyoonkim.main.preference.NotificationPreferencesScreen
+import com.doyoonkim.main.preference.OssNoticeScreen
+import com.doyoonkim.main.preference.UserPreferenceScreen
+import com.doyoonkim.main.viewmodel.CustomerServiceViewModel
+import com.doyoonkim.main.viewmodel.HomeViewModel
+import com.doyoonkim.main.viewmodel.NoticeDetailViewModel
+import com.doyoonkim.main.viewmodel.NoticeSearchViewModel
+import com.doyoonkim.main.viewmodel.NoticesInCategoryViewModel
+import com.doyoonkim.main.viewmodel.NotificationPreferencesViewModel
+import com.doyoonkim.model.NoticeCategory
+
+fun NavGraphBuilder.mainServiceNavGraph(
+ navController: NavController,
+ viewModelFactory: ViewModelProvider.Factory,
+ contentPadding: PaddingValues,
+ onNoticeDetailRequested: (NoticeDetail) -> Unit,
+ onBookmarkServiceRequested: (BookmarkInfo) -> Unit
+) {
+ // ViewModels will be injected via ViewModelFactory
+ composable(NavRoutes.Home.route) {
+ HomeScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ bottomPadding = contentPadding.calculateBottomPadding(),
+ onGoBackAction = { navController.popBackStack() },
+ onMoreNoticeRequested = { dest ->
+ navController.run {
+ when(dest) {
+ Destination.MORE_GENERAL -> navigate(NavRoutes.GeneralNotices.route)
+ Destination.MORE_ACADEMIC -> navigate(NavRoutes.AcademicNotices.route)
+ Destination.MORE_SCHOLARSHIP -> navigate(NavRoutes.ScholarshipNotices.route)
+ Destination.MORE_EVENT -> navigate(NavRoutes.EventNotices.route)
+ else -> { /* DO NOTHING. */ }
+ }
+ }
+ },
+ onFullContentRequested = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url)) }
+ )
+ }
+
+ composable(NavRoutes.NoticeSearch.route) {
+ NoticeSearchScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackPressed = { navController.popBackStack() },
+ onNoticeSelected = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url))
+ }
+ )
+ }
+
+ composable(NavRoutes.GeneralNotices.route) {
+ NoticesInCategoryScreen(
+ modifier = Modifier.padding(5.dp),
+ category = NoticeCategory.GENERAL_NEWS,
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackButtonPressed = { navController.popBackStack() },
+ onNoticeSelected = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url))
+ }
+ )
+ }
+
+ composable(NavRoutes.AcademicNotices.route) {
+ NoticesInCategoryScreen(
+ modifier = Modifier.padding(5.dp),
+ category = NoticeCategory.ACADEMIC_NEWS,
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackButtonPressed = { navController.popBackStack() },
+ onNoticeSelected = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url))
+ }
+ )
+ }
+
+ composable(NavRoutes.ScholarshipNotices.route) {
+ NoticesInCategoryScreen(
+ modifier = Modifier.padding(5.dp),
+ category = NoticeCategory.SCHOLARSHIP_NEWS,
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackButtonPressed = { navController.popBackStack() },
+ onNoticeSelected = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url))
+ }
+ )
+ }
+
+ composable(NavRoutes.EventNotices.route) {
+ NoticesInCategoryScreen(
+ modifier = Modifier.padding(5.dp),
+ category = NoticeCategory.EVENT_NEWS,
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackButtonPressed = { navController.popBackStack() },
+ onNoticeSelected = { id, url ->
+ onNoticeDetailRequested(NoticeDetail(id, url))
+ }
+ )
+ }
+
+ // preferences
+ composable(NavRoutes.Settings.route) {
+ UserPreferenceScreen(
+ modifier = Modifier.padding(5.dp),
+ onNotificationPreferenceClicked = { navController.navigate(NavRoutes.NotificationPreferences.route) },
+ onCustomerServiceClicked = { navController.navigate(NavRoutes.CustomerService.route) },
+ onOssClicked = { navController.navigate(NavRoutes.OpenSource.route) },
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+
+ composable(NavRoutes.NotificationPreferences.route) {
+ NotificationPreferencesScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+
+ composable(NavRoutes.CustomerService.route) {
+ CustomerServiceScreen(
+ modifier = Modifier.padding(5.dp),
+ viewModel = viewModel(factory = viewModelFactory),
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+
+ composable(NavRoutes.OpenSource.route) {
+ OssNoticeScreen(
+ modifier = Modifier.padding(5.dp),
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+
+ // DeepLinks for NoticeDetailScreen
+ composable(
+ route = "noticeDetail/{nttId}/{contentUrl}/{isFabVisible}",
+ deepLinks = listOf(
+ navDeepLink {
+ uriPattern = "knutice://service/noticeDetail/{nttId}/{contentUrl}/{isFabVisible}"
+ }
+ )
+ ) { backStackEntry ->
+ val noticeInfo = backStackEntry.arguments?.let {
+ Triple(
+ it.getString("nttId")?.toInt() ?: 0,
+ Uri.decode(it.getString("contentUrl") ?: ""),
+ it.getString("isFabVisible").toBoolean() ?: false
+ )
+ } ?: Triple(0, "", false)
+
+ NoticeDetailScreen(
+ modifier = Modifier.fillMaxSize(),
+ viewModel = viewModel(factory = viewModelFactory),
+ noticeInfo = noticeInfo,
+ onBookmarkCreate = { onBookmarkServiceRequested(BookmarkInfo(
+ noticeId = it.nttId,
+ noticeTitle = it.title,
+ noticeInfo = "[${it.departName}] ${it.timestamp}"
+ )) },
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/di/MainModule.kt b/feature/main/src/main/java/com/doyoonkim/main/di/MainModule.kt
new file mode 100644
index 00000000..734716d7
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/di/MainModule.kt
@@ -0,0 +1,48 @@
+package com.doyoonkim.main.di
+
+import androidx.lifecycle.ViewModel
+import com.doyoonkim.common.di.ViewModelKey
+import com.doyoonkim.main.viewmodel.CustomerServiceViewModel
+import com.doyoonkim.main.viewmodel.HomeViewModel
+import com.doyoonkim.main.viewmodel.NoticeDetailViewModel
+import com.doyoonkim.main.viewmodel.NoticeSearchViewModel
+import com.doyoonkim.main.viewmodel.NoticesInCategoryViewModel
+import com.doyoonkim.main.viewmodel.NotificationPreferencesViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class MainModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(HomeViewModel::class)
+ abstract fun bindsHomeViewModel(viewModel: HomeViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(NoticeDetailViewModel::class)
+ abstract fun bindsNoticeDetailViewModel(viewModel: NoticeDetailViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(NoticeSearchViewModel::class)
+ abstract fun bindsNoticeSearchViewModel(viewModel: NoticeSearchViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(NoticesInCategoryViewModel::class)
+ abstract fun bindsNoticesInCategoryViewModel(viewModel: NoticesInCategoryViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(NotificationPreferencesViewModel::class)
+ abstract fun bindsNotificationPreferencesViewModel(viewModel: NotificationPreferencesViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(CustomerServiceViewModel::class)
+ abstract fun bindsCustomerServiceViewModel(viewModel:CustomerServiceViewModel): ViewModel
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt b/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt
similarity index 73%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt
rename to feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt
index 1ba6620b..a20af81e 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt
@@ -1,13 +1,13 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.home
-import android.content.res.Configuration
-import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
@@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -23,28 +24,28 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import com.doyoonkim.knutice.model.Destination
-import com.doyoonkim.knutice.model.Notice
-import com.doyoonkim.knutice.presentation.component.NotificationPreviewCard
-import com.doyoonkim.knutice.ui.theme.notificationType1
-import com.doyoonkim.knutice.ui.theme.notificationType2
-import com.doyoonkim.knutice.ui.theme.notificationType3
-import com.doyoonkim.knutice.ui.theme.notificationType4
-import com.doyoonkim.knutice.ui.theme.subTitle
-import com.doyoonkim.knutice.viewModel.CategorizedNotificationViewModel
-import com.doyoonkim.knutice.R
+import com.doyoonkim.common.theme.notificationType1
+import com.doyoonkim.common.theme.notificationType2
+import com.doyoonkim.common.theme.notificationType3
+import com.doyoonkim.common.theme.notificationType4
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.R
+import com.doyoonkim.common.navigation.Destination
+import com.doyoonkim.common.ui.NotificationPreviewCard
+import com.doyoonkim.main.viewmodel.HomeViewModel
+import com.doyoonkim.model.NoticeVO
@Composable
-fun CategorizedNotification(
+fun HomeScreen(
modifier: Modifier = Modifier,
- viewModel: CategorizedNotificationViewModel = hiltViewModel(),
- onGoBackAction: () -> Boolean,
+ viewModel: HomeViewModel,
+ bottomPadding: Dp = 0.dp,
+ onGoBackAction: () -> Unit,
onMoreNoticeRequested: (Destination) -> Unit,
- onFullContentRequested: (Notice) -> Unit
+ onFullContentRequested: (Int, String) -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
@@ -53,6 +54,10 @@ fun CategorizedNotification(
onGoBackAction()
}
+ LaunchedEffect(Unit) {
+ viewModel.getTopThreeNotices()
+ }
+
Column(
modifier = modifier.verticalScroll(
rememberScrollState(0)
@@ -63,38 +68,44 @@ fun CategorizedNotification(
NotificationPreviewList (
listTitle = stringResource(R.string.general_news),
titleColor = MaterialTheme.colorScheme.notificationType1,
+ isContentLoading = uiState.isLoading,
contents = uiState.notificationGeneral,
onMoreClicked = { onMoreNoticeRequested(Destination.MORE_GENERAL) }
) {
- onFullContentRequested(it)
+ onFullContentRequested(it.nttId, it.url)
}
NotificationPreviewList(
listTitle = stringResource(R.string.academic_news),
titleColor = MaterialTheme.colorScheme.notificationType2,
+ isContentLoading = uiState.isLoading,
contents = uiState.notificationAcademic,
onMoreClicked = { onMoreNoticeRequested(Destination.MORE_ACADEMIC) }
) {
- onFullContentRequested(it)
+ onFullContentRequested(it.nttId, it.url)
}
NotificationPreviewList(
listTitle = stringResource(R.string.scholarship_news),
titleColor = MaterialTheme.colorScheme.notificationType3,
+ isContentLoading = uiState.isLoading,
contents = uiState.notificationScholarship,
onMoreClicked = { onMoreNoticeRequested(Destination.MORE_SCHOLARSHIP) }
) {
- onFullContentRequested(it)
+ onFullContentRequested(it.nttId, it.url)
}
NotificationPreviewList(
listTitle = stringResource(R.string.event_news),
titleColor = MaterialTheme.colorScheme.notificationType4,
+ isContentLoading = uiState.isLoading,
contents = uiState.notificationEvent,
onMoreClicked = { onMoreNoticeRequested(Destination.MORE_EVENT) }
) {
- onFullContentRequested(it)
+ onFullContentRequested(it.nttId, it.url)
}
+
+ Spacer(Modifier.height(bottomPadding))
}
}
@@ -103,9 +114,10 @@ fun NotificationPreviewList(
modifier: Modifier = Modifier,
listTitle: String = "List Title goes here",
titleColor: Color = Color.Unspecified,
- contents: List = listOf(),
+ isContentLoading: Boolean = false,
+ contents: List = listOf(),
onMoreClicked: () -> Unit = { },
- onNoticeClicked: (Notice) -> Unit
+ onNoticeClicked: (NoticeVO) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth()
@@ -114,10 +126,7 @@ fun NotificationPreviewList(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
- Modifier.fillMaxWidth(
-
- )
- .wrapContentHeight(),
+ Modifier.fillMaxWidth().wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@@ -145,20 +154,11 @@ fun NotificationPreviewList(
contents.forEach { content ->
NotificationPreviewCard(
notificationTitle = content.title,
- notificationInfo = "[${content.departName}] ${content.timestamp}"
+ notificationInfo = "[${content.departName}] ${content.timestamp}",
+ isLoading = isContentLoading
) {
onNoticeClicked(content)
}
}
}
-}
-
-@Composable
-@Preview(showBackground = true, showSystemUi = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
-fun CategorizedNotification_Preview() {
- NotificationPreviewList(
- contents = listOf(Notice(), Notice(), Notice())
- ) {
-
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt
similarity index 64%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt
rename to feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt
index 8d5474e4..a821da9d 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeDetailScreen.kt
@@ -1,10 +1,9 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.notice
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
@@ -16,56 +15,59 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
+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.wrapContentHeight
-import androidx.compose.material3.LinearProgressIndicator
+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.wrapContentSize
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
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
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
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
import androidx.core.net.toUri
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.doyoonkim.main.viewmodel.NoticeDetailViewModel
+import com.doyoonkim.model.NoticeVO
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.textPurple
@Composable
-fun DetailedNoticeContent(
+fun NoticeDetailScreen(
modifier: Modifier = Modifier,
- viewModel: DetailedNoticeContentViewModel = hiltViewModel(),
+ viewModel: NoticeDetailViewModel,
+ noticeInfo: Triple,
+ onBookmarkCreate: (NoticeVO) -> Unit,
onBackPressed: () -> Unit
) {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
BackHandler { onBackPressed() }
- Column(
- modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.displayBackground),
- verticalArrangement = Arrangement.Top,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- LinearProgressIndicator(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
- progress = {
- state.loadingStatue
- }
- )
+ LaunchedEffect(uiState.isReceived) {
+ if (!uiState.isReceived) viewModel.getTargetNoticeById(noticeInfo.first)
+ }
- if (state.url.isNotBlank() || state.requestedNotice.url != "Unknown") {
+ if (noticeInfo.second.isNotBlank()) {
+ Box(
+ modifier = modifier
+ ) {
AndroidView(
- modifier = Modifier,
+ modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
//Enable Javascript
@@ -142,21 +144,63 @@ fun DetailedNoticeContent(
visibility = View.INVISIBLE
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
- if (state.url != "Unknown") {
- loadUrl(state.url)
- } else {
- loadUrl(state.requestedNotice.url)
- }
+ loadUrl(noticeInfo.second)
}
}
)
+
+ if (noticeInfo.third) {
+ FloatingActionButton(
+ modifier = Modifier.wrapContentSize()
+ .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom))
+ .padding(end = 10.dp, bottom = 30.dp)
+ .align(Alignment.BottomEnd),
+ onClick = {
+ if (uiState.isReceived) uiState.receivedNotice?.let(onBookmarkCreate)
+ },
+ containerColor = if (uiState.isReceived) {
+ MaterialTheme.colorScheme.textPurple
+ } else {
+ MaterialTheme.colorScheme.subTitle
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = "Add to bookmark",
+ tint = Color.White
+ )
+ }
+ }
}
}
}
-
-
-@Preview
+@Preview(showSystemUi = true, showBackground = true)
@Composable
-fun DetailedNoticeContent_Preview() {
+fun NoticeDetailScreen_Preview() {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ FloatingActionButton(
+ modifier = Modifier.wrapContentSize()
+ .padding(end = 10.dp, bottom = 30.dp)
+ .align(Alignment.BottomEnd),
+ onClick = {
+ // TODO Set proper paramter for onBookmarkCreate()
+// if (true) uiState.receivedNotice?.let()
+ },
+ containerColor = if (true) {
+ MaterialTheme.colorScheme.textPurple
+ } else {
+ MaterialTheme.colorScheme.subTitle
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = "Add to bookmark",
+ tint = Color.White
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt
similarity index 65%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt
rename to feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt
index ab1c114b..0dac7b72 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticeSearchScreen.kt
@@ -1,6 +1,5 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.notice
-import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
@@ -27,51 +26,34 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-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.presentation.component.NotificationPreview
-import com.doyoonkim.knutice.ui.theme.containerBackground
-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.SearchNoticeViewModel
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
+import com.doyoonkim.main.viewmodel.NoticeSearchViewModel
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.containerBackground
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.textPurple
+import com.doyoonkim.common.theme.title
+import com.doyoonkim.common.ui.NotificationPreview
-@OptIn(FlowPreview::class)
@Composable
-fun SearchNotice(
+fun NoticeSearchScreen(
modifier: Modifier = Modifier,
- viewModel: SearchNoticeViewModel = hiltViewModel(),
- onBackClicked: () -> Unit = { },
- onNoticeClicked: (Notice) -> Unit
+ viewModel: NoticeSearchViewModel,
+ onBackPressed: () -> Unit,
+ onNoticeSelected: (Int, String) -> Unit
) {
-
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- LaunchedEffect(Unit) {
- snapshotFlow{ uiState.searchKeyword }
- .debounce(500L)
- .distinctUntilChanged()
- .filter { it.isNotBlank() }
- .collectLatest {
- viewModel.queryNoticeByKeyword(it)
- }
- }
+ BackHandler { onBackPressed() }
- BackHandler { onBackClicked() }
+ LaunchedEffect(uiState.searchKeyword) {
+ viewModel.observeKeywordInput()
+ }
Column(
modifier = modifier
@@ -92,7 +74,7 @@ fun SearchNotice(
.padding(2.dp),
value = uiState.searchKeyword,
placeholder = { Text(stringResource(R.string.title_search)) },
- onValueChange = { viewModel.updateKeyword(it) },
+ onValueChange = { viewModel.updateSearchKeyword(it) },
colors = TextFieldDefaults.colors(
focusedTextColor = MaterialTheme.colorScheme.title,
unfocusedTextColor = MaterialTheme.colorScheme.subTitle,
@@ -107,59 +89,51 @@ fun SearchNotice(
}
Box(
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier
+ .fillMaxSize()
.align(Alignment.CenterHorizontally)
) {
LazyColumn(
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(3.dp)
) {
- items(uiState.queryResult) { notice ->
+ items(uiState.fetchResult) { notice ->
HorizontalDivider(
Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp),
color = MaterialTheme.colorScheme.containerBackground
)
Row(
- modifier = Modifier.fillMaxWidth().wrapContentHeight()
- .clickable {
- onNoticeClicked(notice)
- }
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onNoticeSelected(notice.nttId, notice.url) }
) {
NotificationPreview(
modifier = Modifier.fillMaxWidth(),
- isImageContained = notice.imageUrl != "Unknown",
+ isLoading = uiState.isFetching,
+ isImageContained = notice.imageUrl != null,
notificationTitle = notice.title,
notificationInfo = "[${notice.departName}] ${notice.timestamp}",
- imageUrl = notice.imageUrl
+ imageUrl = notice.imageUrl ?: ""
)
}
-
}
}
- // Loading Indicator w/ AnimatedVisibility
- androidx.compose.animation.AnimatedVisibility(
- visible = uiState.isQuerying,
+ androidx.compose.animation.AnimatedVisibility(
+ visible = uiState.isFetching,
modifier = Modifier.wrapContentSize().align(Alignment.Center),
enter = scaleIn(),
- exit = scaleOut(),
+ exit = scaleOut()
) {
CircularProgressIndicator(
- color = MaterialTheme.colorScheme.textPurple
+ color = MaterialTheme.colorScheme.textPurple,
+ trackColor = MaterialTheme.colorScheme.containerBackground
)
}
}
}
-}
-
-@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES.and(Configuration.UI_MODE_NIGHT_MASK), showSystemUi = true)
-@Composable
-fun SearchNotice_Preview(
-) {
- SearchNotice {
-
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt
similarity index 62%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt
rename to feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt
index 50ac36f6..20c10a69 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/notice/NoticesInCategoryScreen.kt
@@ -1,6 +1,5 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.notice
-import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -12,13 +11,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
-//noinspection UsingMaterialAndMaterial3Libraries
-import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -26,34 +24,38 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
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
+import com.doyoonkim.common.theme.containerBackground
+import com.doyoonkim.common.theme.containerBackgroundSolid
+import com.doyoonkim.common.theme.textPurple
+import com.doyoonkim.common.ui.NotificationPreview
+import com.doyoonkim.main.viewmodel.NoticesInCategoryViewModel
+import com.doyoonkim.model.NoticeCategory
@OptIn(ExperimentalMaterialApi::class)
@Composable
-fun MoreCategorizedNotification(
- modifier: Modifier = Modifier,
- viewModel: MoreCategorizedNotificationViewModel = hiltViewModel(),
- backButtonHandler: () -> Unit = { },
- onNoticeSelected: (Notice) -> Unit = { }
+fun NoticesInCategoryScreen(
+ modifier: Modifier,
+ category: NoticeCategory = NoticeCategory.Unspecified,
+ viewModel: NoticesInCategoryViewModel,
+ onBackButtonPressed: () -> Unit = { },
+ onNoticeSelected: (Int, String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ // Back Handler
+ BackHandler { onBackButtonPressed() }
+
+ // Pull-to-Refresh
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.isRefreshRequested,
- onRefresh = {
- viewModel.requestRefresh()
- }
+ onRefresh = { viewModel.requestRefresh() }
)
- BackHandler {
- backButtonHandler()
+ // Fetching notification on entry
+ LaunchedEffect(uiState.isNoticesRequested, uiState.isRefreshRequested) {
+ if (uiState.isRefreshRequested || uiState.isNoticesRequested)
+ viewModel.getNoticesPerPageInCategory(category)
}
Box(
@@ -61,12 +63,6 @@ fun MoreCategorizedNotification(
.background(MaterialTheme.colorScheme.containerBackground)
.pullRefresh(pullRefreshState)
) {
- LaunchedEffect(Unit) {
- Log.d("MoreCategorizedNotification", "Initialize Notice Category to be fetched")
-// if (uiState.currentLastNttId == 0)
-// viewModel.setNotificationCategory(category)
- viewModel.fetchNotificationPerPage()
- }
LazyColumn(
Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(5.dp),
@@ -75,40 +71,38 @@ fun MoreCategorizedNotification(
items(uiState.notices.size) { index ->
if (index == uiState.notices.size - 1) {
Row(
- modifier = Modifier.fillMaxWidth()
- .wrapContentHeight(),
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.wrapContentSize(),
- color = MaterialTheme.colorScheme.subTitle
+ color = MaterialTheme.colorScheme.textPurple,
+ trackColor = MaterialTheme.colorScheme.containerBackground
)
}
viewModel.requestMoreNotices()
} else {
- val notice = uiState.notices[index]
- if (index != 0 || !uiState.isLoading) {
- Divider(
- Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp),
- color = MaterialTheme.colorScheme.containerBackgroundSolid,
- thickness = 1.3.dp
+ if (index != 0) {
+ HorizontalDivider(
+ Modifier.fillMaxWidth(),
+ color =MaterialTheme.colorScheme.containerBackgroundSolid,
+ thickness = 1.2.dp
)
}
+ val notice = uiState.notices[index]
Row(
modifier = Modifier.wrapContentSize()
- .clickable {
- onNoticeSelected(notice)
- }
+ .clickable { onNoticeSelected(notice.nttId, notice.url) }
) {
NotificationPreview(
+ isLoading = uiState.isLoading,
notificationTitle = notice.title,
notificationInfo = "[${notice.departName}] ${notice.timestamp}",
- isImageContained = notice.imageUrl != "Unknown",
- imageUrl = notice.imageUrl
+ isImageContained = notice.imageUrl != null,
+ imageUrl = notice.imageUrl ?: ""
)
}
-
}
}
}
@@ -119,5 +113,4 @@ fun MoreCategorizedNotification(
state = pullRefreshState
)
}
-}
-
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/CustomerServiceScreen.kt
similarity index 78%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt
rename to feature/main/src/main/java/com/doyoonkim/main/preference/CustomerServiceScreen.kt
index cd628425..ddce10dd 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/preference/CustomerServiceScreen.kt
@@ -1,5 +1,6 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.preference
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
@@ -7,25 +8,29 @@ 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.WindowInsetsSides
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.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.shape.RoundedCornerShape
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,30 +38,37 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import com.doyoonkim.knutice.R
-import com.doyoonkim.knutice.ui.theme.containerBackground
-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.CustomerServiceViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.doyoonkim.main.viewmodel.CustomerServiceViewModel
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.buttonContainer
+import com.doyoonkim.common.theme.buttonPurple
+import com.doyoonkim.common.theme.containerBackground
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.textPurple
+import com.doyoonkim.common.theme.title
@Composable
-fun CustomerService(
- modifier: Modifier = Modifier,
- viewModel: CustomerServiceViewModel = hiltViewModel(),
- onSubmitRequested: (String) -> Unit = {},
- onCloseRequested: () -> Unit = {}
+fun CustomerServiceScreen(
+ modifier: Modifier = Modifier,
+ viewModel: CustomerServiceViewModel,
+ onBackPressed: () -> Unit
) {
- val uiState by viewModel.uiState.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ // Version Information for Report Submission
+ val versionInfo = stringResource(R.string.version_code)
val adjustImePadding = Modifier.consumeWindowInsets(WindowInsets.ime).imePadding()
+
+ BackHandler {
+ onBackPressed()
+ }
+
Box(
modifier = modifier.fillMaxSize()
- .windowInsetsPadding(WindowInsets.systemBars)
+ .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom))
) {
Column(
verticalArrangement = Arrangement.Top,
@@ -115,10 +127,18 @@ fun CustomerService(
}
Button(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 3.dp, end = 3.dp)
+ ,
enabled = !uiState.isSubmissionCompleted && uiState.exceedMinCharacters,
shape = RoundedCornerShape(10.dp),
- onClick = { viewModel.submitUserReport() }
+ colors = ButtonDefaults.buttonColors().copy(
+ containerColor = MaterialTheme.colorScheme.buttonPurple,
+ contentColor = Color.White,
+ ),
+ onClick = { viewModel.submitUserReport(versionInfo) }
) {
Text(
text = stringResource(R.string.btn_submit),
@@ -156,7 +176,7 @@ fun CustomerService(
text = stringResource(R.string.submission_completed__subtitle)
)
Button(
- onClick = { viewModel.updateCompletionState() },
+ onClick = { viewModel.resetSubmissionStatus() },
modifier = Modifier.fillMaxWidth()
) {
Text(
@@ -171,10 +191,4 @@ fun CustomerService(
}
}
-}
-
-@Composable
-@Preview(showSystemUi = true, locale = "ko-rKR",)
-fun CustomerService_Preview() {
-
}
\ No newline at end of file
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
new file mode 100644
index 00000000..cd52c3a4
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/preference/NotificationPreferencesScreen.kt
@@ -0,0 +1,207 @@
+package com.doyoonkim.main.preference
+
+import android.Manifest
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Context.NOTIFICATION_SERVICE
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Paint.Align
+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.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.layout.wrapContentWidth
+import androidx.compose.material3.Text
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+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
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LifecycleEventEffect
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.buttonPurple
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
+import com.doyoonkim.common.ui.LabeledToggleSwitch
+import com.doyoonkim.main.viewmodel.NotificationPreferencesViewModel
+
+@Composable
+fun NotificationPreferencesScreen(
+ modifier: Modifier = Modifier,
+ viewModel: NotificationPreferencesViewModel,
+ onBackPressed: () -> Unit
+) {
+ // Context for main status checking
+ val context = LocalContext.current
+ val uiStatus by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ isMainNotificationPermissionGranted(
+ context,
+ onResult = { viewModel.updateMainNotificationPermissionStatus(it) }
+ )
+ viewModel.getTopicSubscriptionStatus()
+ }
+
+ LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
+ // Once user leave the application for permission settings and getting back.
+ isMainNotificationPermissionGranted(
+ context,
+ onResult = { viewModel.updateMainNotificationPermissionStatus(it) }
+ )
+ }
+
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top
+ ) {
+ 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
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ 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
+ )
+ }
+ }
+
+ 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
+ ) {
+ 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
+ ) {
+ 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
+ ) {
+ viewModel.updateChannelPreferenceState(3, it)
+ }
+
+ }
+
+}
+
+fun isMainNotificationPermissionGranted(
+ context: Context,
+ onResult: (Boolean) -> Unit
+) {
+ val isNotificationAllowed = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+
+ val isChannelAllowed = (context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
+ .getNotificationChannel(context.getString(R.string.inapp_notification_channel_id))
+ .importance > 0
+
+ if (isNotificationAllowed && isChannelAllowed) onResult(true)
+ else onResult(false)
+}
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
new file mode 100644
index 00000000..3f923c8d
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt
@@ -0,0 +1,24 @@
+package com.doyoonkim.main.preference
+
+import android.webkit.WebView
+import androidx.activity.compose.BackHandler
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+
+@Composable
+fun OssNoticeScreen(
+ modifier: Modifier = Modifier,
+ onBackPressed: () -> Unit
+) {
+ BackHandler { onBackPressed() }
+
+ AndroidView(
+ modifier = modifier,
+ factory = { context ->
+ WebView(context).apply {
+ loadUrl("https://knutice.github.io/KNUTICE-OpenSourceLicense/Android/opensource.html")
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt
similarity index 80%
rename from app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt
rename to feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt
index 08dc255c..68ace30c 100644
--- a/app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt
+++ b/feature/main/src/main/java/com/doyoonkim/main/preference/UserPreferencesScreen.kt
@@ -1,8 +1,7 @@
-package com.doyoonkim.knutice.presentation
+package com.doyoonkim.main.preference
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -25,20 +24,17 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.doyoonkim.knutice.model.Destination
-import com.doyoonkim.knutice.ui.theme.buttonContainer
-import com.doyoonkim.knutice.ui.theme.subTitle
-import com.doyoonkim.knutice.R
-import com.doyoonkim.knutice.ui.theme.title
-
-// TODO: Apply Color Theme on HorizontalDivider.
+import com.doyoonkim.common.R
+import com.doyoonkim.common.theme.buttonContainer
+import com.doyoonkim.common.theme.subTitle
+import com.doyoonkim.common.theme.title
@Composable
-fun UserPreference(
+fun UserPreferenceScreen(
modifier: Modifier = Modifier,
- onNotificationPreferenceClicked: (Destination) -> Unit,
- onCustomerServiceClicked: (Destination) -> Unit,
- onOssClicked: (Destination) -> Unit,
+ onNotificationPreferenceClicked: () -> Unit,
+ onCustomerServiceClicked: () -> Unit,
+ onOssClicked: () -> Unit,
onBackPressed: () -> Unit,
) {
BackHandler { onBackPressed() }
@@ -49,7 +45,8 @@ fun UserPreference(
verticalArrangement = Arrangement.Top
) {
Text(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ modifier = Modifier.fillMaxWidth().wrapContentHeight()
+ .padding(bottom = 2.dp),
text = stringResource(R.string.pref_notification_title),
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
@@ -63,11 +60,7 @@ fun UserPreference(
)
Column(
- modifier = Modifier.fillMaxWidth().wrapContentSize()
- .padding(top = 15.dp, bottom = 15.dp)
- .clickable {
- onNotificationPreferenceClicked(Destination.NOTIFICATION)
- },
+ modifier = Modifier.fillMaxWidth().wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -75,13 +68,23 @@ fun UserPreference(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ modifier = Modifier.wrapContentHeight().weight(5f),
text = stringResource(R.string.enable_notification_title),
fontWeight = FontWeight.Medium,
fontSize = 18.sp,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.title
)
+
+ IconButton(
+ onClick = { onNotificationPreferenceClicked() }
+ ) {
+ Image(
+ painter = painterResource(R.drawable.baseline_arrow_forward_ios_24),
+ contentDescription = "Go to App Notification Preferences page.",
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.buttonContainer)
+ )
+ }
}
}
@@ -92,7 +95,7 @@ fun UserPreference(
Text(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
- .padding(top = 20.dp),
+ .padding(top = 20.dp, bottom = 2.dp),
text = stringResource(R.string.title_support),
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
@@ -106,8 +109,7 @@ fun UserPreference(
)
Row(
- modifier = Modifier.fillMaxWidth().wrapContentSize()
- .padding(top = 5.dp, bottom = 5.dp),
+ modifier = Modifier.fillMaxWidth().wrapContentSize(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -120,7 +122,7 @@ fun UserPreference(
)
IconButton(
- onClick = { onCustomerServiceClicked(Destination.CS) }
+ onClick = { onCustomerServiceClicked() }
) {
Image(
painter = painterResource(R.drawable.baseline_arrow_forward_ios_24),
@@ -137,7 +139,7 @@ fun UserPreference(
Text(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
- .padding(top = 20.dp),
+ .padding(top = 20.dp, bottom = 2.dp),
text = stringResource(R.string.about_title),
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
@@ -152,7 +154,7 @@ fun UserPreference(
Row(
modifier = Modifier.fillMaxWidth().wrapContentSize()
- .padding(top = 15.dp, bottom = 15.dp),
+ .padding(top = 12.5.dp, bottom = 12.5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
@@ -168,7 +170,7 @@ fun UserPreference(
modifier = Modifier.wrapContentHeight().weight(3f),
text = stringResource(R.string.version_code),
fontWeight = FontWeight.Normal,
- fontSize = 14.sp,
+ fontSize = 16.sp,
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.title
)
@@ -180,8 +182,7 @@ fun UserPreference(
)
Row(
- modifier = Modifier.fillMaxWidth().wrapContentSize()
- .padding(top = 5.dp, bottom = 5.dp),
+ modifier = Modifier.fillMaxWidth().wrapContentSize(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -194,7 +195,7 @@ fun UserPreference(
)
IconButton(
- onClick = { onOssClicked(Destination.OSS) }
+ onClick = { onOssClicked() }
) {
Image(
painter = painterResource(R.drawable.baseline_arrow_forward_ios_24),
@@ -212,9 +213,14 @@ fun UserPreference(
}
}
-
-@Preview(showSystemUi = true, locale = "KO")
+@Preview(showBackground = true, showSystemUi = true)
@Composable
-fun UserPreference_Preview() {
-
+fun UserPreferencesScreen_Preview() {
+ UserPreferenceScreen(
+ modifier = Modifier.fillMaxWidth().padding(10.dp),
+ onNotificationPreferenceClicked = { },
+ onCustomerServiceClicked = { },
+ onOssClicked = { },
+ onBackPressed = { }
+ )
}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/CustomerServiceViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/CustomerServiceViewModel.kt
new file mode 100644
index 00000000..b64649a4
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/CustomerServiceViewModel.kt
@@ -0,0 +1,77 @@
+package com.doyoonkim.main.viewmodel
+
+import android.os.Build
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.SubmitUserReportImpl
+import com.doyoonkim.model.requestBody.UserReportBody
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class CustomerServiceViewModel @Inject constructor(
+ private val submitUserReport: SubmitUserReportImpl
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(CustomerServiceStatus())
+ val uiState = _uiState.asStateFlow()
+
+ fun updateUserReportContent(content: String) =
+ _uiState.update {
+ it.copy(
+ userReport = content,
+ exceedMinCharacters = content.length >= 5,
+ reachedMaxCharacters = content.length >= 500
+ )
+ }
+
+ fun submitUserReport(versionInfo: String) =
+ viewModelScope.launch {
+ submitUserReport(
+ UserReportBody(
+ content = uiState.value.userReport,
+ deviceName = "${Build.BRAND} ${Build.MODEL}",
+ version = versionInfo
+ )
+ ).flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ _uiState.update {
+ if (result) {
+ it.copy(
+ userReport = "",
+ reachedMaxCharacters = false,
+ isSubmissionCompleted = true,
+ isSubmissionFailed = false
+ )
+ } else {
+ it.copy(
+ isSubmissionFailed = true,
+ isSubmissionCompleted = true
+ )
+ }
+ }
+ }
+ }
+
+ fun resetSubmissionStatus() =
+ _uiState.update {
+ it.copy(
+ isSubmissionFailed = false,
+ isSubmissionCompleted = false
+ )
+ }
+
+}
+
+data class CustomerServiceStatus(
+ val userReport: String = "",
+ val reachedMaxCharacters: Boolean = false,
+ val exceedMinCharacters: Boolean = false,
+ val isSubmissionFailed: Boolean = false,
+ val isSubmissionCompleted: Boolean = false
+)
\ No newline at end of file
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
new file mode 100644
index 00000000..689dae5f
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/HomeViewModel.kt
@@ -0,0 +1,49 @@
+package com.doyoonkim.main.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchTopThreeNoticesImpl
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+//import javax.inject.Inject
+
+
+class HomeViewModel @Inject constructor(
+ private val fetchTopThreeNotices: FetchTopThreeNoticesImpl
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(HomeViewState())
+ val uiState = _uiState.asStateFlow()
+
+ fun getTopThreeNotices() = viewModelScope.launch {
+ fetchTopThreeNotices()
+ .flowOn(Dispatchers.IO)
+ .collectLatest { vo ->
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ notificationGeneral = vo.general,
+ notificationScholarship = vo.scholarship,
+ notificationAcademic = vo.academic,
+ notificationEvent = vo.event
+ )
+ }
+ }
+ }
+}
+
+data class HomeViewState(
+ val isLoading: Boolean = true,
+ val notificationGeneral: List = listOf(NoticeVO(), NoticeVO(), NoticeVO()),
+ val notificationAcademic: List = listOf(NoticeVO(), NoticeVO(), NoticeVO()),
+ val notificationScholarship: List = listOf(NoticeVO(), NoticeVO(), NoticeVO()),
+ val notificationEvent: List = listOf(NoticeVO(), NoticeVO(), NoticeVO())
+)
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeDetailViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeDetailViewModel.kt
new file mode 100644
index 00000000..37cd1764
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeDetailViewModel.kt
@@ -0,0 +1,53 @@
+package com.doyoonkim.main.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchNoticeByIdImpl
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class NoticeDetailViewModel @Inject constructor(
+ private val fetchNoticeById: FetchNoticeByIdImpl
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(NoticeDetailState())
+ val uiState = _uiState.asStateFlow()
+
+ fun getTargetNoticeById(nttId:Int) =
+ viewModelScope.launch {
+ fetchNoticeById(nttId)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ _uiState.update {
+ it.copy(
+ receivedNotice = result,
+ isReceived = true
+ )
+ }
+ }
+ }
+
+ fun updateLoadingStatus(newStatus: Int) {
+ viewModelScope.launch {
+ _uiState.update {
+ it.copy(
+ loadingStatus = (newStatus / 100).toFloat()
+ )
+ }
+ }
+ }
+
+}
+
+data class NoticeDetailState(
+ val receivedNotice: NoticeVO? = null,
+ val isReceived: Boolean = false,
+ val loadingStatus: Float = 0.0f
+)
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeSearchViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeSearchViewModel.kt
new file mode 100644
index 00000000..e9b379e6
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticeSearchViewModel.kt
@@ -0,0 +1,68 @@
+package com.doyoonkim.main.viewmodel
+
+import androidx.compose.runtime.snapshotFlow
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchNoticesByKeywordImpl
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class NoticeSearchViewModel @Inject constructor(
+ private val fetchNoticesByKeyword: FetchNoticesByKeywordImpl
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(NoticeSearchState())
+ val uiState = _uiState.asStateFlow()
+
+ fun searchNoticeUsingKeyword(keyword: String) {
+ viewModelScope.launch {
+ fetchNoticesByKeyword(keyword)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ _uiState.update {
+ it.copy(
+ isFetching = false,
+ fetchResult = result
+ )
+ }
+ }
+ }
+ }
+
+ @OptIn(FlowPreview::class)
+ suspend fun observeKeywordInput() = snapshotFlow { uiState.value.searchKeyword }
+ .debounce(500L)
+ .distinctUntilChanged()
+ .filter { it.isNotBlank() }
+ .collectLatest {
+ searchNoticeUsingKeyword(it)
+ }
+
+ fun updateSearchKeyword(newKeyword: String) {
+ _uiState.update {
+ it.copy(
+ searchKeyword = newKeyword
+ )
+ }
+ }
+
+ private fun updateFetchingStatus(status: Boolean) = _uiState.update { it.copy(isFetching = status) }
+
+}
+
+data class NoticeSearchState(
+ val searchKeyword: String = "",
+ val isFetching: Boolean = false,
+ val fetchResult: List = emptyList()
+)
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticesInCategoryViewModel.kt b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticesInCategoryViewModel.kt
new file mode 100644
index 00000000..c8fad649
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NoticesInCategoryViewModel.kt
@@ -0,0 +1,76 @@
+package com.doyoonkim.main.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchNoticesPerPageImpl
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.model.NoticeVO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class NoticesInCategoryViewModel @Inject constructor(
+ private val fetchNoticesPerPage: FetchNoticesPerPageImpl
+) : ViewModel() {
+
+ // uiStates
+ private var _uiState = MutableStateFlow(NoticesInCategoryStates())
+ val uiState = _uiState.asStateFlow()
+
+ fun getNoticesPerPageInCategory(category: NoticeCategory) =
+ viewModelScope.launch {
+ fetchNoticesPerPage(category, uiState.value.currentLastNttId)
+ .flowOn(Dispatchers.IO)
+ .collectLatest { result ->
+ _uiState.update {
+ it.copy(
+ currentLastNttId = result.last().nttId,
+ notices =
+ if (uiState.value.currentLastNttId == 0)
+ result
+ else
+ it.notices.addAll(result),
+ isNoticesRequested = false,
+ isLoading = false,
+ isRefreshRequested = false
+ )
+ }
+ }
+ }
+
+ fun requestRefresh() =
+ _uiState.update {
+ it.copy(
+ currentLastNttId = 0,
+ notices = emptyList(),
+ isRefreshRequested = true
+ )
+ }
+
+ // TODO Subject to be removed.
+ fun requestMoreNotices() {
+ // TODO Need to fix the way to pass the NoticeCategory parameter.
+ if (!uiState.value.isLoading) _uiState.update { it.copy(isNoticesRequested = true) }
+ }
+
+
+ private fun List.addAll(extra: List) =
+ List(this.size + extra.size) {
+ if (it < this.size) this[it]
+ else extra[it - this.size]
+ }
+
+}
+
+data class NoticesInCategoryStates(
+ val currentLastNttId: Int = 0,
+ val notices: List = List(20) { NoticeVO() },
+ val isNoticesRequested: Boolean = true,
+ val isLoading: Boolean = true,
+ val isRefreshRequested: Boolean = false
+)
\ No newline at end of file
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
new file mode 100644
index 00000000..8e8e9501
--- /dev/null
+++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt
@@ -0,0 +1,105 @@
+package com.doyoonkim.main.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.doyoonkim.domain.usecases.FetchTopicSubscriptionStatusImpl
+import com.doyoonkim.domain.usecases.SubmitNotificationPreferencesImpl
+import com.doyoonkim.model.NoticeCategory
+import com.doyoonkim.model.requestBody.TopicSubscriptionPreferencesBody
+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.collectLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class NotificationPreferencesViewModel @Inject constructor(
+ private val submitNotificationPreferences: SubmitNotificationPreferencesImpl,
+ private val fetchTopicSubscriptionStatus: FetchTopicSubscriptionStatusImpl
+) : ViewModel() {
+
+ private var _uiState = MutableStateFlow(NotificationPreferencesState())
+ val uiState = _uiState.asStateFlow()
+
+ private val notificationChannels = hashMapOf(
+ 0 to NoticeCategory.GENERAL_NEWS,
+ 1 to NoticeCategory.ACADEMIC_NEWS,
+ 2 to NoticeCategory.SCHOLARSHIP_NEWS,
+ 3 to NoticeCategory.EVENT_NEWS
+ )
+
+ fun updateMainNotificationPermissionStatus(status: Boolean) =
+ _uiState.update {
+ it.copy(
+ isMainNotificationPermissionGranted = status
+ )
+ }
+
+ fun updateChannelPreferenceState(index: Int, state: Boolean) {
+ _uiState.update {
+ it.copy(
+ isEachChannelAllowed = it.isEachChannelAllowed.updateValueByIndex(index, state)
+ )
+ }
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val jobSubmit = launch {
+ // Ignore the result.
+ submitNotificationPreferences(
+ TopicSubscriptionPreferencesBody(
+ noticeName = notificationChannels[index]!!.name,
+ isSubscribed = state
+ )
+ ).collectLatest { result ->
+ if (!result) {
+ _uiState.update {
+ it.copy(
+ isEachChannelAllowed = it.isEachChannelAllowed.updateValueByIndex(index, !state),
+ isError = true
+ )
+ }
+ }
+ }
+ }
+ delay(5000L)
+ if (!jobSubmit.isCompleted) jobSubmit.cancelAndJoin()
+ }
+ }
+
+ fun getTopicSubscriptionStatus() =
+ viewModelScope.launch {
+ fetchTopicSubscriptionStatus()
+ .flowOn(Dispatchers.IO)
+ .collectLatest { status ->
+ _uiState.update {
+ it.copy(
+ isEachChannelAllowed = listOf(
+ status.general,
+ status.academic,
+ status.scholarship,
+ status.event
+ )
+ )
+ }
+ }
+ }
+
+ private fun List.updateValueByIndex(index: Int, value: Boolean) =
+ List(this.size) {
+ if (it == index) value
+ else this[it]
+ }
+
+}
+
+data class NotificationPreferencesState(
+ val isMainNotificationPermissionGranted: Boolean = false,
+ val isEachChannelAllowed: List = listOf(false, false, false, false),
+ val isSyncCompleted: Boolean = false,
+ val isError: Boolean = false
+)
\ No newline at end of file
diff --git a/feature/main/src/test/java/com/doyoonkim/main/ExampleUnitTest.kt b/feature/main/src/test/java/com/doyoonkim/main/ExampleUnitTest.kt
new file mode 100644
index 00000000..4f2d1718
--- /dev/null
+++ b/feature/main/src/test/java/com/doyoonkim/main/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.doyoonkim.main
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9f01aaf5..b980c224 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,30 +1,26 @@
[versions]
-agp = "8.6.0"
+agp = "8.6.1"
coilCompose = "2.4.0"
-converterGson = "2.3.0"
-converterMoshi = "2.9.0"
-datastorePreferences = "1.1.1"
+converterGson = "2.11.0"
+datastorePreferences = "1.1.7"
firebaseMessagingDirectboot = "24.0.2"
-gson = "2.11.0"
-hiltAndroidCompiler = "2.51.1"
-hiltNavigationCompose = "1.2.0"
jsoup = "1.18.1"
-kotlin = "1.9.21"
-coreKtx = "1.10.1"
+kotlin = "2.1.20"
+coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
-kotlinxCoroutinesAndroid = "1.7.1"
+kotlinxCoroutinesAndroid = "1.7.3"
kotlinxCoroutinesTest = "1.7.3"
-lifecycleRuntimeKtx = "2.6.1"
-activityCompose = "1.8.0"
-composeBom = "2024.04.01"
-lifecycleRuntimeKtxVersion = "2.8.6"
-navigationCompose = "2.8.1"
+lifecycleRuntimeKtx = "2.9.0"
+activityCompose = "1.10.1"
+composeBom = "2025.05.01"
+lifecycleRuntimeKtxVersion = "2.9.0"
+navigationCompose = "2.9.0"
retrofit = "2.9.0"
-roomCompiler = "2.6.1"
-roomKtx = "2.6.1"
-roomRuntime = "2.6.1"
+roomCompiler = "2.7.1"
+roomKtx = "2.7.1"
+roomRuntime = "2.7.1"
swiperefreshlayout = "1.1.0"
googleGmsGoogleServices = "4.4.2"
firebaseMessaging = "24.0.2"
@@ -32,14 +28,16 @@ kotlinSerialization = "1.6.0"
junitKtx = "1.2.1"
protoliteWellKnownTypes = "18.0.0"
translate = "17.0.3"
+appcompat = "1.7.0"
+material = "1.12.0"
+dagger = "2.56.2"
splashScreen = "1.0.1"
+jetbrainsKotlinJvm = "2.1.20"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashScreen" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
-androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
-androidx-lifecycle-runtime-ktx-v286 = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtxVersion" }
androidx-material = { module = "androidx.compose.material:material" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
@@ -48,13 +46,7 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" }
firebase-messaging-directboot = { module = "com.google.firebase:firebase-messaging-directboot", version.ref = "firebaseMessagingDirectboot" }
-google-firebase-messaging = { module = "com.google.firebase:firebase-messaging" }
-gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" }
-hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
-jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -70,12 +62,20 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesAndroid" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref="kotlinSerialization" }
androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" }
protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" }
translate = { module = "com.google.mlkit:translate", version.ref = "translate" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
+dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
+dagger-android = { module = "com.google.dagger:dagger-android", version.ref = "dagger" }
+dagger-android-support = { module = "com.google.dagger:dagger-android-support", version.ref = "dagger" }
+dagger-android-processor = { module = "com.google.dagger:dagger-android-processor", version.ref = "dagger" }
[plugins]
@@ -83,3 +83,6 @@ android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e8df23b0..3f9177cf 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,5 +19,15 @@ dependencyResolutionManagement {
}
}
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
rootProject.name = "KNUTICE"
include(":app")
+include(":core:network")
+include(":core:data")
+include(":feature:main")
+include(":feature:bookmark")
+include(":common")
+include(":core:notification")
+include(":core:domain")
+include(":core:model")