diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6bf8b6de..ddfb50c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,25 +25,18 @@ android { namespace = "com.doyoonkim.knutice" compileSdk = 35 - val properties = Properties().apply { - load(FileInputStream("${rootDir}/local.properties")) - } - val apiMigrated = properties["api_migrated"] ?: "" - defaultConfig { applicationId = "com.doyoonkim.knutice" minSdk = 31 targetSdk = 35 - versionCode = 26 - versionName = "1.5.0" + versionCode = 27 + versionName = "1.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } - buildConfigField("String", "API_MIGRATED", "\"$apiMigrated\"") - javaCompileOptions { annotationProcessorOptions { arguments["room.schemaLocation"] = "$projectDir/schemas" @@ -59,6 +52,10 @@ android { "proguard-rules.pro" ) } + + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 @@ -93,8 +90,8 @@ dependencies { 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.activity.compose) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) @@ -104,14 +101,24 @@ dependencies { implementation(libs.firebase.messaging.directboot) implementation(libs.androidx.junit.ktx) implementation(libs.protolite.well.known.types) + implementation(libs.firebase.analytics) + + testImplementation(platform(libs.androidx.compose.bom)) testImplementation(libs.junit) testImplementation(libs.kotlinx.coroutines.test) // Library to test coroutines in JUnit + testImplementation(libs.mockk) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.compose.ui.test) + + androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.mockk.android) androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.test.manifest) // Dagger implementation(libs.dagger) @@ -120,6 +127,11 @@ dependencies { kapt(libs.dagger.compiler) kapt(libs.dagger.android.processor) + // Dagger for Android Test + androidTestImplementation(libs.dagger) + androidTestImplementation(libs.dagger.compiler) + kaptAndroidTest(libs.dagger.compiler) + implementation(libs.kotlin.serialization) // Coroutine for Android diff --git a/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt b/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt index 3aaa5d24..170ddf68 100644 --- a/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt +++ b/app/src/main/java/com/doyoonkim/knutice/MainActivity.kt @@ -3,7 +3,6 @@ package com.doyoonkim.knutice import android.Manifest import android.app.AlarmManager import android.content.Intent -import android.net.Uri import android.os.Bundle import android.provider.Settings import android.util.Log @@ -32,15 +31,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.core.view.WindowCompat -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.doyoonkim.common.navigation.NavRoutes import com.doyoonkim.common.theme.KNUTICETheme import com.doyoonkim.common.ui.PermissionRationaleComposable import com.doyoonkim.common.R -import com.doyoonkim.common.di.AppPreferences +import com.doyoonkim.common.navigation.DeeplinkHandler import com.doyoonkim.common.theme.onAnyBackground import com.doyoonkim.common.theme.variantPurple import com.doyoonkim.knutice.di.components.DaggerMainActivityComponent @@ -48,7 +45,7 @@ import com.doyoonkim.knutice.di.components.DaggerSplashSceneComponent import com.doyoonkim.knutice.di.util.DefaultSystemService import com.doyoonkim.main.splash.KnuticeSplashScreen import com.doyoonkim.main.viewmodel.SplashViewModel -import com.doyoonkim.notification.local.NotificationAlarmScheduler +import com.google.firebase.analytics.FirebaseAnalytics import kotlinx.coroutines.delay import javax.inject.Inject @@ -65,6 +62,7 @@ class MainActivity : ComponentActivity() { val appComponent = (application as MainApplication).appComponent DaggerMainActivityComponent.factory().create(DefaultSystemService(appComponent)) .inject(this) + val analytics = appComponent.analytics() super.onCreate(savedInstanceState) receivedIntent.value = intent @@ -76,7 +74,7 @@ class MainActivity : ComponentActivity() { val context = LocalContext.current navController = rememberNavController() - var lastProcessedIntent by remember { mutableStateOf(receivedIntent.value.hashCode()) } + var lastProcessedIntent by remember { mutableStateOf(null) } var isDeeplinkInProcess by remember { mutableStateOf(false) } var isPreProcessCompleted by remember { mutableStateOf(false) } @@ -170,16 +168,25 @@ class MainActivity : ComponentActivity() { } } - LaunchedEffect(receivedIntent.value) { - receivedIntent.value?.let { intent -> - if (intent.hashCode() != lastProcessedIntent && isPreProcessCompleted) { - isDeeplinkInProcess = true - lastProcessedIntent = intent.hashCode() - - intent.data?.let { uri -> - navController.navigate(uri.navDestination()) + LaunchedEffect(receivedIntent.value, isPreProcessCompleted) { + if (isPreProcessCompleted) { + receivedIntent.value?.let { intent -> + if (intent.hashCode() != lastProcessedIntent) { + isDeeplinkInProcess = true + lastProcessedIntent = intent.hashCode() + + DeeplinkHandler.processIntent(intent) { service, uri -> + // Analytics + analytics.logEvent("CLICK_NOTIFICATION", Bundle().apply { + putString(FirebaseAnalytics.Param.CONTENT_TYPE, service) + putString(FirebaseAnalytics.Param.SOURCE, "PUSH") + putString(FirebaseAnalytics.Param.DESTINATION, uri) + }) + + navController.navigate(uri) + } + isDeeplinkInProcess = false } - isDeeplinkInProcess = false } } } @@ -195,14 +202,6 @@ class MainActivity : ComponentActivity() { receivedIntent.value = intent } - private fun Uri.navDestination(): String { - return if (this.host != "service") { - NavRoutes.Home.route - } else { - this.encodedPath?.substring(1) ?: NavRoutes.Home.route - } - } - override fun onDestroy() { super.onDestroy() diff --git a/app/src/main/java/com/doyoonkim/knutice/analytics/AnalyticsLogger.kt b/app/src/main/java/com/doyoonkim/knutice/analytics/AnalyticsLogger.kt new file mode 100644 index 00000000..cd7cbef2 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/analytics/AnalyticsLogger.kt @@ -0,0 +1,19 @@ +package com.doyoonkim.knutice.analytics + +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics +import javax.inject.Inject + +interface AnalyticsLogger { + fun logEvent(event: String, param: Bundle? = null) +} + +class FirebaseAnalyticsLogger @Inject constructor( + private val analytics: FirebaseAnalytics +) : AnalyticsLogger { + + override fun logEvent(event: String, param: Bundle?) { + analytics.logEvent(event, param) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/di/components/AppComponent.kt b/app/src/main/java/com/doyoonkim/knutice/di/components/AppComponent.kt index 7426faac..87c58a6c 100644 --- a/app/src/main/java/com/doyoonkim/knutice/di/components/AppComponent.kt +++ b/app/src/main/java/com/doyoonkim/knutice/di/components/AppComponent.kt @@ -7,13 +7,16 @@ import android.content.Context import android.content.SharedPreferences import com.doyoonkim.common.di.ApplicationContext import com.doyoonkim.knutice.MainApplication +import com.doyoonkim.knutice.analytics.AnalyticsLogger import com.doyoonkim.knutice.di.modules.AppModule +import com.doyoonkim.knutice.di.modules.FirebaseAnalyticsModule import dagger.BindsInstance import dagger.Component @Component( modules = [ - AppModule::class + AppModule::class, + FirebaseAnalyticsModule::class ] ) interface AppComponent { @@ -26,6 +29,9 @@ interface AppComponent { fun alarmManager(): AlarmManager fun notificationManager(): NotificationManager + // Analytics + fun analytics(): AnalyticsLogger + @Component.Factory interface Factory { // provide AppComponent diff --git a/app/src/main/java/com/doyoonkim/knutice/di/modules/FirebaseAnalyticsModule.kt b/app/src/main/java/com/doyoonkim/knutice/di/modules/FirebaseAnalyticsModule.kt new file mode 100644 index 00000000..f2a73e38 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/di/modules/FirebaseAnalyticsModule.kt @@ -0,0 +1,25 @@ +package com.doyoonkim.knutice.di.modules + +import android.content.Context +import com.doyoonkim.common.di.ApplicationContext +import com.doyoonkim.knutice.analytics.AnalyticsLogger +import com.doyoonkim.knutice.analytics.FirebaseAnalyticsLogger +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.Binds +import dagger.Module +import dagger.Provides + +@Module +abstract class FirebaseAnalyticsModule { + + companion object { + @Provides + fun providesAnalyticsInstance(@ApplicationContext context: Context) = FirebaseAnalytics.getInstance(context) + } + + @Binds + abstract fun bindsFirebaseAnalyticsLogger( + impl: FirebaseAnalyticsLogger + ): AnalyticsLogger + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/navigation/MainServiceNavGraph.kt b/app/src/main/java/com/doyoonkim/knutice/navigation/MainServiceNavGraph.kt index 9f986ca1..da7c7f3a 100644 --- a/app/src/main/java/com/doyoonkim/knutice/navigation/MainServiceNavGraph.kt +++ b/app/src/main/java/com/doyoonkim/knutice/navigation/MainServiceNavGraph.kt @@ -1,6 +1,7 @@ package com.doyoonkim.knutice.navigation import android.net.Uri +import android.os.Bundle import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.EaseIn import androidx.compose.animation.core.EaseOut @@ -47,6 +48,7 @@ import com.doyoonkim.main.viewmodel.NoticesInCategoryViewModel import com.doyoonkim.main.viewmodel.NotificationPreferencesViewModel import com.doyoonkim.main.viewmodel.SettingsViewModel import com.doyoonkim.model.NoticeCategory +import com.google.firebase.analytics.FirebaseAnalytics fun NavGraphBuilder.mainServiceNavGraph( navController: NavController, @@ -56,7 +58,7 @@ fun NavGraphBuilder.mainServiceNavGraph( onBookmarkServiceRequested: (BookmarkInfo) -> Unit, onExit: () -> Unit = { } ) { - + val analytics = appComponent.analytics() // ViewModels will be injected via ViewModelFactory composable(NavRoutes.Home.route) { @@ -88,6 +90,11 @@ fun NavGraphBuilder.mainServiceNavGraph( onNoticeDetailRequested(NoticeDetail(id, url)) }, onTipClicked = { category, url -> + analytics.logEvent("BROWSE_TIP", Bundle().apply { + putString(FirebaseAnalytics.Param.ITEM_CATEGORY, category.name) + putString(FirebaseAnalytics.Param.SOURCE, "HomeScreen") + putString(FirebaseAnalytics.Param.DESTINATION, url) + }) navController.navigate("tipDetail/${category.name}/${Uri.encode(url)}") } ) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 1534d547..8027c990 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -28,6 +28,9 @@ android { "proguard-rules.pro" ) } + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/common/src/main/java/com/doyoonkim/common/navigation/DeeplinkHandler.kt b/common/src/main/java/com/doyoonkim/common/navigation/DeeplinkHandler.kt new file mode 100644 index 00000000..ded1f344 --- /dev/null +++ b/common/src/main/java/com/doyoonkim/common/navigation/DeeplinkHandler.kt @@ -0,0 +1,40 @@ +package com.doyoonkim.common.navigation + +import android.content.Intent +import android.net.Uri + +class DeeplinkHandler { + companion object { + fun processIntent( + intent: Intent, + onDestination: (service: String, uri: String) -> Unit + ) { + var destination = NavRoutes.Home.route + + if (intent.data != null) { + destination = intent.data!!.navDestination() + } else { + val id = intent.getStringExtra("nttId") + val url = intent.getStringExtra("contentUrl") + + if (id != null && url != null) { + destination = generateNoticeUri(id, url) + } + } + + val service = destination.split("/")[0] + onDestination(service, destination) + } + + private fun Uri.navDestination(): String { + return if (this.host != "service") { + NavRoutes.Home.route + } else { + this.encodedPath?.substring(1) ?: NavRoutes.Home.route + } + } + + private fun generateNoticeUri(id: String, url: String) = + "noticeDetail/$id/${Uri.encode(url)}/${true}" + } +} \ No newline at end of file diff --git a/common/src/main/java/com/doyoonkim/common/ui/TipContainer.kt b/common/src/main/java/com/doyoonkim/common/ui/TipContainer.kt index 50a3c2da..25b3b898 100644 --- a/common/src/main/java/com/doyoonkim/common/ui/TipContainer.kt +++ b/common/src/main/java/com/doyoonkim/common/ui/TipContainer.kt @@ -1,33 +1,24 @@ package com.doyoonkim.common.ui -import android.util.Log -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement 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.foundation.shape.RoundedCornerShape 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 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.text.intl.Locale 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 com.doyoonkim.common.R -import com.doyoonkim.common.theme.containerGray -import com.doyoonkim.common.theme.onAnyBackground import com.doyoonkim.common.theme.title enum class TipCategory { UPDATES, SYS_NOTICE, GENERAL_TIP } @@ -36,9 +27,7 @@ enum class TipCategory { UPDATES, SYS_NOTICE, GENERAL_TIP } fun TipContainer( modifier: Modifier = Modifier, tipCategory: TipCategory = TipCategory.UPDATES, - containerColor: Color, - tipText: String = "", - onTipClicked: () -> Unit + tipText: String = "" ) { val tagTitle = when(tipCategory) { TipCategory.GENERAL_TIP -> stringResource(R.string.text_tip_general) @@ -46,48 +35,45 @@ fun TipContainer( TipCategory.SYS_NOTICE -> stringResource(R.string.text_tip_sys_notice) } - - Surface( - modifier = modifier.background(Color.Transparent) - .clickable { onTipClicked() }, - color = containerColor, - shape = RoundedCornerShape(15.dp) + Row( + modifier = modifier.wrapContentHeight() + .padding( + horizontal = 10.dp, + vertical = 7.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Row( - modifier = Modifier.wrapContentHeight() - .padding(7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp) +/* + Surface( + modifier = Modifier.wrapContentSize() + .background(Color.Transparent), + color = MaterialTheme.colorScheme.containerGray, + shape = RoundedCornerShape(15.dp) ) { - Surface( - modifier = Modifier.wrapContentSize() - .background(Color.Transparent), - color = MaterialTheme.colorScheme.containerGray, - shape = RoundedCornerShape(15.dp) - ) { - Text( - modifier = Modifier.wrapContentSize().padding( - vertical = 5.dp, - horizontal = 10.dp - ), - text = tagTitle, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.title - ) - } Text( - modifier = Modifier.wrapContentSize(), - text = tipText, + modifier = Modifier.wrapContentSize().padding( + vertical = 5.dp, + horizontal = 10.dp + ), + text = tagTitle, fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, + fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis + color = MaterialTheme.colorScheme.title ) } + */ + Text( + modifier = Modifier.wrapContentSize(), + text = tipText, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } } @@ -97,7 +83,6 @@ fun TipContainer_Preview() { TipContainer( modifier = Modifier.fillMaxWidth().wrapContentHeight(), tipCategory = TipCategory.SYS_NOTICE, - containerColor = MaterialTheme.colorScheme.onAnyBackground, tipText = "1.4.2 업데이트 이후 푸시 알림이 표출되지 않는 문제" - ) { } + ) } \ No newline at end of file diff --git a/common/src/main/java/com/doyoonkim/common/ui/TipPager.kt b/common/src/main/java/com/doyoonkim/common/ui/TipPager.kt new file mode 100644 index 00000000..f21bae55 --- /dev/null +++ b/common/src/main/java/com/doyoonkim/common/ui/TipPager.kt @@ -0,0 +1,105 @@ +package com.doyoonkim.common.ui + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import com.doyoonkim.common.theme.onAnyBackground +import com.doyoonkim.model.TipVO +import kotlinx.coroutines.delay +import kotlin.math.absoluteValue + +@Composable +fun TipPager( + modifier: Modifier, + tips: List = emptyList(), + onTipClicked: (String) -> Unit +) { + // Pager State + val pagerState = rememberPagerState(pageCount = { Int.MAX_VALUE }) + + val pageInteractionSource = remember { MutableInteractionSource() } + val isPagePressed by pageInteractionSource.collectIsPressedAsState() + + // Auto-Advancing + val isAutoAdvancing = !isPagePressed && tips.size > 1 + if (isAutoAdvancing) { + LaunchedEffect(pagerState, pageInteractionSource) { + while (true) { + delay(5000L) + with(pagerState.currentPage) { + if (this < Int.MAX_VALUE) { + pagerState.animateScrollToPage(this + 1) + } + } + } + } + } + + Surface( + modifier = Modifier.fillMaxSize() + .wrapContentHeight() + .background(Color.Transparent) + .clickable { + onTipClicked( + tips[pagerState.currentPage % tips.size].url + ) + }, + color = MaterialTheme.colorScheme.onAnyBackground, + shape = RoundedCornerShape(15.dp) + ) { + // Vertical Pager + VerticalPager( + modifier = modifier, + pageSize = PageSize.Fixed(50.dp), + userScrollEnabled = false, + state = pagerState + ) { index -> + Log.d("TipPager", "Current Page: $index State Status: ${pagerState.currentPage}") + Log.d("TipPager", "INDEX?: ${index % pagerState.pageCount}") + TipContainer( + modifier = Modifier.fillMaxWidth().height(50.dp) + .graphicsLayer { + val pageOffset = ( + (pagerState.currentPage - index) + pagerState.currentPageOffsetFraction + ).absoluteValue + + alpha = lerp( + start = 0.5f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + }, + tipCategory = TipCategory.UPDATES, + tipText = tips[index % tips.size].title + ) + } + } +} + +@Composable +@Preview(showBackground = true) +fun TipPager_Preview() { + +} \ No newline at end of file diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index b35b4c74..63922933 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -91,4 +91,6 @@ リクエストの処理中にエラーが発生しました。 就職アラム 新しい就職公知があればアラムを取りたい + 更新が新しい順 + 更新が古い順 \ No newline at end of file diff --git a/common/src/main/res/values-ko-rKR/strings.xml b/common/src/main/res/values-ko-rKR/strings.xml index 6de925a7..733b5ab6 100644 --- a/common/src/main/res/values-ko-rKR/strings.xml +++ b/common/src/main/res/values-ko-rKR/strings.xml @@ -91,4 +91,6 @@ 요청을 처리하는 중 오류가 발생했어요. 취업 공지 알림 채용 정보, 취업 지원 프로그램, 진로 상담 등 학생들의 진로 설계를 돕기 위한 소식을 알려드려요 + 최근 수정순 + 오래된 수정순 \ No newline at end of file diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 9d8ba5ec..63e12997 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ About Version Open Source License - 1.5.0 + 1.5.1 Notification Preference New Notice has been delivered! @@ -102,4 +102,6 @@ Error occurred while processing your request. Career Notices Get push notification when new career notice is being posted. + Newest Updated + Oldest Updated \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e8110b83..9c435dbc 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -25,6 +25,9 @@ android { "proguard-rules.pro" ) } + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 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 index 49018c94..1f40f9c8 100644 --- a/core/data/src/main/java/com/doyoonkim/data/repository/LocalRepositoryImpl.kt +++ b/core/data/src/main/java/com/doyoonkim/data/repository/LocalRepositoryImpl.kt @@ -76,9 +76,13 @@ class LocalRepositoryImpl @Inject constructor( override fun queryBookmarkSorted(size: Int, pageNumber: Int, option: SortOption) = flow { runCatching { - when (option) { - SortOption.ASC_CREATION -> localDao.getBookmarkListSortedNewest(size, pageNumber) - SortOption.DES_CREATION -> localDao.getBookmarkListSortedOldest(size, pageNumber) + with (localDao) { + when (option) { + SortOption.ASC_CREATION -> getBookmarkListSortedNewest(size, pageNumber) + SortOption.DES_CREATION -> getBookmarkListSortedOldest(size, pageNumber) + SortOption.ASC_UPDATED -> getBookmarkListSortedUpdatedNewest(size, pageNumber) + SortOption.DESC_UPDATED -> getBookmarkListSortedUpdatedOldest(size, pageNumber) + } } }.onFailure { throw it }.fold( onSuccess = { dto -> diff --git a/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt b/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt index c918c5b9..7e994a41 100644 --- a/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt +++ b/core/data/src/main/java/com/doyoonkim/data/room/MainDatabaseDao.kt @@ -78,4 +78,34 @@ interface MainDatabaseDao { """) fun getBookmarkListSortedOldest(size: Int, pageNumber: Int): List + @Query(""" + SELECT + b.bookmarkId AS bookmarkId, + n.ntt_id AS noticeId, + n.notice_title AS noticeTitle, + n.notice_category AS noticeCategory, + b.isScheduled AS isReminderSet, + b.created_at AS createdAt, + b.updated_at AS updatedAt + FROM Bookmark b + INNER JOIN NoticeEntity n ON n.ntt_id = b.target_ntt_id + ORDER BY b.updated_at ASC LIMIT :size OFFSET :pageNumber * :size + """) + fun getBookmarkListSortedUpdatedNewest(size: Int, pageNumber: Int): List + + @Query(""" + SELECT + b.bookmarkId AS bookmarkId, + n.ntt_id AS noticeId, + n.notice_title AS noticeTitle, + n.notice_category AS noticeCategory, + b.isScheduled AS isReminderSet, + b.created_at AS createdAt, + b.updated_at AS updatedAt + FROM Bookmark b + INNER JOIN NoticeEntity n ON n.ntt_id = b.target_ntt_id + ORDER BY b.updated_at DESC LIMIT :size OFFSET :pageNumber * :size + """) + fun getBookmarkListSortedUpdatedOldest(size: Int, pageNumber: Int): List + } \ No newline at end of file diff --git a/core/domain/src/main/java/com/doyoonkim/domain/SortOption.kt b/core/domain/src/main/java/com/doyoonkim/domain/SortOption.kt index 88fb3c9f..940dd849 100644 --- a/core/domain/src/main/java/com/doyoonkim/domain/SortOption.kt +++ b/core/domain/src/main/java/com/doyoonkim/domain/SortOption.kt @@ -1,2 +1,2 @@ package com.doyoonkim.domain -enum class SortOption { DES_CREATION, ASC_CREATION } \ No newline at end of file +enum class SortOption { DES_CREATION, ASC_CREATION, DESC_UPDATED, ASC_UPDATED } \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 2aa05030..bb52e9d8 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -15,6 +15,7 @@ android { load(FileInputStream("${rootDir}/local.properties")) } val apiBaseLive = properties["api_migrated"] ?: "" + val apiBaseTest = properties["api_migrated_test"] ?: "" defaultConfig { minSdk = 30 @@ -33,6 +34,11 @@ android { "proguard-rules.pro" ) } + + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + buildConfigField("String", "API_LIVE", "\"$apiBaseTest\"") + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 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 index 508367ea..67a9c39e 100644 --- a/core/network/src/main/kotlin/com/doyoonkim/network/retrofit/KnuticeService.kt +++ b/core/network/src/main/kotlin/com/doyoonkim/network/retrofit/KnuticeService.kt @@ -25,16 +25,16 @@ import retrofit2.http.Query */ interface KnuticeService { - @GET("open-api/notice") + @GET("open-api/notices/latest") suspend fun getTopThreeNotices(): TopThreeNoticeResults - @GET("open-api/notice/list") + @GET("open-api/notices") suspend fun getNoticesPerPage( @Query("noticeName") category: NoticeCategory, @Query("nttId") lastNttId: Int? = null ): NoticesPerPageResult - @GET("open-api/notice/{nttId}") + @GET("open-api/notices/{nttId}") suspend fun getNoticeById( @Path("nttId") nttId: String ): NoticeByIdResult @@ -45,7 +45,7 @@ interface KnuticeService { @Query("nttId") lastNttId: Int? = null ): NoticesByKeywordResult - @GET("open-api/topic") + @GET("open-api/topics/status") suspend fun getTopicSubscriptionStatus( @Header("fcmToken") token: String ): TopicSubscriptionPreferencesResult @@ -56,19 +56,19 @@ interface KnuticeService { ): TipResult @Headers("Content-Type: application/json") - @POST("open-api/fcm") + @POST("open-api/fcm/tokens") suspend fun validateToken( @Body request: DeviceTokenRequest ): PostResult @Headers("Content-Type: application/json") - @POST("open-api/report") + @POST("open-api/reports") suspend fun submitUserReport( @Body request: UserReportRequest ): PostResult @Headers("Content-Type: application/json") - @POST("open-api/topic") + @POST("open-api/topics/subscription") suspend fun submitTopicSubscriptionPreferences( @Body request: TopicSubscriptionPreferencesRequest ): PostResult diff --git a/core/notification/build.gradle.kts b/core/notification/build.gradle.kts index 0ad1f7da..27d46b3a 100644 --- a/core/notification/build.gradle.kts +++ b/core/notification/build.gradle.kts @@ -25,6 +25,9 @@ android { "proguard-rules.pro" ) } + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 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 index cfc1e541..be4c77e4 100644 --- a/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationHandler.kt +++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationHandler.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout @@ -46,16 +45,10 @@ class PushNotificationHandler @Inject constructor( 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}") + Log.d(TAG, "Notification payload: ${message.notification}") + Log.d(TAG, "Data payload: ${message.data}") - 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?) - } + message.toPushNotification() } fun inactivateCoroutineScope() { @@ -65,16 +58,36 @@ class PushNotificationHandler @Inject constructor( @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) private fun RemoteMessage.toPushNotification() { + val notification = this@toPushNotification.notification + val data = this@toPushNotification.data + + 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) + setContentTitle(notification?.title ?: context.getString(R.string.new_notice)) + setContentText(notification?.body ?: context.getString(R.string.text_push_to_notice)) + setPriority(NotificationCompat.PRIORITY_DEFAULT) + setAutoCancel(true) + } // Create Pending Intent (For access push notification while the app is in foreground) - val nttId = this@toPushNotification.data["nttId"] - val url = this@toPushNotification.data["contentUrl"] + val nttId = data["nttId"] + val url = data["contentUrl"] val fabVisible = true + val uri = if (nttId != null && url != null) { + "knutice://service/noticeDetail/$nttId/${Uri.encode(url)}/$fabVisible".toUri() + } else { + "".toUri() + } + // 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() + uri ).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP } @@ -86,17 +99,8 @@ class PushNotificationHandler @Inject constructor( 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) - setContentTitle(context.getString(R.string.new_notice)) - setContentText(context.getString(R.string.text_push_to_notice)) + notificationBuilder.apply { setContentIntent(pendingIntent) - setPriority(NotificationCompat.PRIORITY_DEFAULT) - setAutoCancel(true) } with(NotificationManagerCompat.from(context)) { @@ -117,47 +121,33 @@ class PushNotificationHandler @Inject constructor( } coroutineScope.launch { - Log.d(TAG, "START FETCHING NOTICE") - nttId?.let { - val notice = async { - remoteRepository.queryNoticeById(it.toInt()) - .firstOrNull() - } - notice.await()?.let { vo -> - Log.d(TAG, "RECEIVED ${vo.toString()}") - notificationBuilder.apply { - setContentTitle(localizedTitle(vo.noticeName)) - setContentText(vo.title) - } - - vo.imageUrl?.let { url -> - val bitmapImage = async { - runCatching { - withTimeout(5000L) { - imageRepository.getImageByteArrayFromUrl(url)?.let { b -> - bitMapHandler.decodeByteArray(b) - } - } + Log.d(TAG, "START FETCHING IMAGE") + notification?.imageUrl?.let { uri -> + val bitmapImage = async { + runCatching { + withTimeout(5000L) { + imageRepository.getImageByteArrayFromUrl(uri.toString())?.let { b -> + bitMapHandler.decodeByteArray(b) } } - bitmapImage.await().fold( - onSuccess = { result -> - result?.let { - notificationBuilder.apply { - setStyle( - NotificationCompat.BigPictureStyle() - .bigPicture(it) - ) - } - } - }, - onFailure = { - Log.d(TAG, "Unable to receive image.\n" + - "REASON: ${it.stackTrace}") - } - ) } } + bitmapImage.await().fold( + onSuccess = { result -> + result?.let { + notificationBuilder.apply { + setStyle( + NotificationCompat.BigPictureStyle() + .bigPicture(it) + ) + } + } + }, + onFailure = { + Log.d(TAG, "Unable to receive image.\n" + + "REASON: ${it.stackTrace}") + } + ) } }.invokeOnCompletion { notify(notificationId, notificationBuilder.build()) } 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 index 394c4e6a..ee25623e 100644 --- a/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationService.kt +++ b/core/notification/src/main/java/com/doyoonkim/notification/fcm/PushNotificationService.kt @@ -30,19 +30,6 @@ class PushNotificationService : FirebaseMessagingService() { // 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) diff --git a/feature/bookmark/build.gradle.kts b/feature/bookmark/build.gradle.kts index a77bd111..a52e12cd 100644 --- a/feature/bookmark/build.gradle.kts +++ b/feature/bookmark/build.gradle.kts @@ -26,6 +26,9 @@ android { "proguard-rules.pro" ) } + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt index 1767c236..cef4cc04 100644 --- a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt +++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/list/BookmarkListScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -47,7 +46,6 @@ import com.doyoonkim.common.navigation.BookmarkInfo import com.doyoonkim.common.R import com.doyoonkim.common.theme.displayBackground import com.doyoonkim.common.theme.onAnyBackground -import com.doyoonkim.common.theme.subTitle import com.doyoonkim.common.theme.title import com.doyoonkim.common.theme.variantPurple import com.doyoonkim.common.ui.NotificationPreviewCardMarked @@ -89,7 +87,9 @@ fun BookmarkListScreen( menuContentColor = MaterialTheme.colorScheme.title, menuOptions = listOf( stringResource(R.string.text_newest), - stringResource(R.string.text_oldest) + stringResource(R.string.text_oldest), + stringResource(R.string.text_updated_newest), + stringResource(R.string.text_updated_oldest) ), onMenuSelected = { index -> if (uiState.bookmarks.isNotEmpty()) { diff --git a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt index 06007163..6e3f918b 100644 --- a/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt +++ b/feature/bookmark/src/main/java/com/doyoonkim/bookmark/viewmodel/BookmarkListViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index eb9b31f5..fcd453a0 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -26,6 +26,10 @@ android { "proguard-rules.pro" ) } + + create("ExperimentalServerDebug") { + initWith(buildTypes["debug"]) + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt index 144bbda1..e77236c2 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/home/HomeScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.IconButton @@ -49,6 +50,7 @@ import com.doyoonkim.common.ui.NotificationPreviewCard import com.doyoonkim.common.ui.PlaceholderScreen import com.doyoonkim.common.ui.TipCategory import com.doyoonkim.common.ui.TipContainer +import com.doyoonkim.common.ui.TipPager import com.doyoonkim.common.ui.TopAppBarWithActions import com.doyoonkim.main.viewmodel.HomeViewModel import com.doyoonkim.model.NoticeVO @@ -105,78 +107,89 @@ fun HomeScreen( contentText = stringResource(R.string.error_no_network_connection) ) } else { - Column( + LazyColumn( modifier = modifier - .padding(innerPadding) - .verticalScroll(rememberScrollState(0)), + .padding(innerPadding), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { if (uiState.tips.isNotEmpty()) { - TipContainer( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - tipCategory = TipCategory.UPDATES, - containerColor = MaterialTheme.colorScheme.onAnyBackground, - tipText = uiState.tips[0].title, - ) { - onTipClicked( - TipCategory.UPDATES, - uiState.tips[0].url - ) + item(key = "header") { + TipPager( + modifier = Modifier.fillMaxWidth().height(50.dp), + tips = uiState.tips + ) { + onTipClicked( + TipCategory.UPDATES, + it + ) + } } } - NotificationPreviewList ( - listTitle = stringResource(R.string.general_news), - titleColor = MaterialTheme.colorScheme.notificationType1, - isContentLoading = uiState.isLoading, - contents = uiState.notificationGeneral, - onMoreClicked = { onMoreNoticeRequested(Destination.MORE_GENERAL) } - ) { - onFullContentRequested(it.nttId, it.url) + item { + NotificationPreviewList ( + listTitle = stringResource(R.string.general_news), + titleColor = MaterialTheme.colorScheme.notificationType1, + isContentLoading = uiState.isLoading, + contents = uiState.notificationGeneral, + onMoreClicked = { onMoreNoticeRequested(Destination.MORE_GENERAL) } + ) { + 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.nttId, it.url) + item { + NotificationPreviewList( + listTitle = stringResource(R.string.academic_news), + titleColor = MaterialTheme.colorScheme.notificationType2, + isContentLoading = uiState.isLoading, + contents = uiState.notificationAcademic, + onMoreClicked = { onMoreNoticeRequested(Destination.MORE_ACADEMIC) } + ) { + 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.nttId, it.url) + item { + NotificationPreviewList( + listTitle = stringResource(R.string.scholarship_news), + titleColor = MaterialTheme.colorScheme.notificationType3, + isContentLoading = uiState.isLoading, + contents = uiState.notificationScholarship, + onMoreClicked = { onMoreNoticeRequested(Destination.MORE_SCHOLARSHIP) } + ) { + 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.nttId, it.url) + item { + NotificationPreviewList( + listTitle = stringResource(R.string.event_news), + titleColor = MaterialTheme.colorScheme.notificationType4, + isContentLoading = uiState.isLoading, + contents = uiState.notificationEvent, + onMoreClicked = { onMoreNoticeRequested(Destination.MORE_EVENT) } + ) { + onFullContentRequested(it.nttId, it.url) + } } - NotificationPreviewList( - listTitle = stringResource(R.string.employment_news), - titleColor = MaterialTheme.colorScheme.notificationType5, - isContentLoading = uiState.isLoading, - contents = uiState.notificationEmployment, - onMoreClicked = { onMoreNoticeRequested(Destination.MORE_EMPLOYMENT) } - ) { - onFullContentRequested(it.nttId, it.url) + item { + NotificationPreviewList( + listTitle = stringResource(R.string.employment_news), + titleColor = MaterialTheme.colorScheme.notificationType5, + isContentLoading = uiState.isLoading, + contents = uiState.notificationEmployment, + onMoreClicked = { onMoreNoticeRequested(Destination.MORE_EMPLOYMENT) } + ) { + onFullContentRequested(it.nttId, it.url) + } } - Spacer(Modifier.height(bottomPadding)) + item { + Spacer(Modifier.height(bottomPadding)) + } } } } diff --git a/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt index b934b2f4..4ea9e29a 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/preference/OssNoticeScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import com.doyoonkim.common.ui.TopAppBarWithBackButton @@ -33,7 +34,7 @@ fun OssNoticeScreen( onBackPressed = onBackPressed ) }, - containerColor = MaterialTheme.colorScheme.displayBackground + containerColor = Color.White ) { innerPadding -> AndroidView( modifier = modifier.fillMaxSize().padding(innerPadding), diff --git a/feature/main/src/main/java/com/doyoonkim/main/tip/TipDetailScreen.kt b/feature/main/src/main/java/com/doyoonkim/main/tip/TipDetailScreen.kt index a3983d07..c5f68e5d 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/tip/TipDetailScreen.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/tip/TipDetailScreen.kt @@ -2,16 +2,20 @@ package com.doyoonkim.main.tip import android.webkit.WebView import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Scaffold 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.res.stringResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.doyoonkim.common.R -import com.doyoonkim.common.theme.displayBackground import com.doyoonkim.common.ui.TipCategory import com.doyoonkim.common.ui.TopAppBarWithBackButton @@ -38,15 +42,22 @@ fun TipDetailScreen( onBackPressed = onBackPressed ) }, - containerColor = MaterialTheme.colorScheme.displayBackground + containerColor = Color.White ) { innerPadding -> - AndroidView( - modifier = modifier.fillMaxSize().padding(innerPadding), - factory = { context -> - WebView(context).apply { - loadUrl(contentUrl) + Box( + modifier = Modifier.wrapContentSize() + .padding(horizontal = 10.dp) + .background(Color.Transparent), + contentAlignment = Alignment.Center + ) { + AndroidView( + modifier = modifier.fillMaxSize().padding(innerPadding), + factory = { context -> + WebView(context).apply { + loadUrl(contentUrl) + } } - } - ) + ) + } } } \ 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 index cf6b9325..79f82962 100644 --- a/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt +++ b/feature/main/src/main/java/com/doyoonkim/main/viewmodel/NotificationPreferencesViewModel.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout import javax.inject.Inject class NotificationPreferencesViewModel @Inject constructor( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f51b6c28..7f2901c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ composeBom = "2025.05.01" lifecycleRuntimeKtxVersion = "2.9.0" navigationCompose = "2.9.0" retrofit = "2.9.0" +robolectric = "4.13" room = "2.7.1" swiperefreshlayout = "1.1.0" googleGmsGoogleServices = "4.4.2" @@ -32,6 +33,7 @@ jetbrainsKotlinJvm = "2.1.20" uiTooling = "1.8.3" mockWebServer = "5.0.0" mockk = "1.14.4" +firebaseAnalytics = "23.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -52,6 +54,8 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } @@ -74,11 +78,13 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = 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" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockWebServer" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockk" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics", version.ref = "firebaseAnalytics" } [plugins]