diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d5852a1..bcc8a7a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,6 +117,7 @@ dependencies { implementation(libs.protolite.well.known.types) implementation(libs.androidx.navigation.compose) implementation(libs.coil.compose) + implementation(libs.androidx.lifecycle.runtime.compose) // 테스트 testImplementation(libs.junit) diff --git a/app/src/androidTest/java/com/example/notimanager/data/service/NotiListenerServiceTest.kt b/app/src/androidTest/java/com/example/notimanager/data/service/NotiListenerServiceTest.kt index 75d912b..6c837c9 100644 --- a/app/src/androidTest/java/com/example/notimanager/data/service/NotiListenerServiceTest.kt +++ b/app/src/androidTest/java/com/example/notimanager/data/service/NotiListenerServiceTest.kt @@ -13,7 +13,7 @@ import com.example.notimanager.data.model.NotificationMetaModel import com.example.notimanager.presentation.ui.activity.MainActivity import com.example.notimanager.data.model.NotificationModel import com.example.notimanager.data.repository.NotificationRepository -import com.example.notimanager.data.service.NotiListenerService +import com.example.notimanager.domain.service.NotiListenerService import com.example.notimanager.data.utils.NameGetter import com.example.notimanager.data.utils.IntentHelper import io.mockk.coEvery @@ -85,7 +85,7 @@ class NotiListenerServiceTest { // Mock the PendingIntentHelper val intentArray = "MockedByteArray".toByteArray() - every { IntentHelper.saveIntent(mockPendingIntent) } returns intentArray + every { IntentHelper.saveIntent(realIntent) } returns intentArray val expectedNotificationModel = NotificationModel( appName = "Test App", @@ -103,12 +103,12 @@ class NotiListenerServiceTest { coEvery { notificationRepository.insertNotificationMeta(notificationMetaModel) } returns 1L val notificationIconModel = NotificationIconModel( notificationId = 1L, - iconBytes = uri + iconBytes = "".toByteArray() ) coEvery { notificationRepository.insertNotificationIcon(notificationIconModel) } returns 1L val appIconModel = AppIconModel( - iconBytes = "com.example.test", + iconBytes = "".toByteArray(), notiAppName = "Test App" ) coEvery { notificationRepository.insertAppIcon(appIconModel) } returns 1L @@ -123,6 +123,6 @@ class NotiListenerServiceTest { coVerify { notificationRepository.insertAppIcon(appIconModel) } // Verify that PendingIntentHelper was called - verify { IntentHelper.saveIntent(mockPendingIntent) } + verify { IntentHelper.saveIntent(realIntent) } } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2383bc..f7d40df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NotiManger" - tools:targetApi="31"> + android:enableOnBackInvokedCallback="true" + tools:targetApi="35"> diff --git a/app/src/main/java/com/example/notimanager/data/repository/NotificationRepository.kt b/app/src/main/java/com/example/notimanager/data/repository/NotificationRepository.kt index 8f7ecba..01e37b0 100644 --- a/app/src/main/java/com/example/notimanager/data/repository/NotificationRepository.kt +++ b/app/src/main/java/com/example/notimanager/data/repository/NotificationRepository.kt @@ -8,6 +8,7 @@ import com.example.notimanager.data.source.local.dao.AppIconDao import com.example.notimanager.data.source.local.dao.NotificationDao import com.example.notimanager.data.source.local.dao.NotificationIconDao import com.example.notimanager.data.source.local.dao.NotificationMetaDao +import com.example.notimanager.domain.repository.NotificationRepositoryInterface class NotificationRepository( private val notificationDao: NotificationDao, diff --git a/app/src/main/java/com/example/notimanager/di/RepositoryModule.kt b/app/src/main/java/com/example/notimanager/di/RepositoryModule.kt index bba488e..c2855aa 100644 --- a/app/src/main/java/com/example/notimanager/di/RepositoryModule.kt +++ b/app/src/main/java/com/example/notimanager/di/RepositoryModule.kt @@ -5,7 +5,7 @@ import com.example.notimanager.data.repository.NotificationAppRepository import com.example.notimanager.data.repository.NotificationPermissionRepository import com.example.notimanager.data.repository.NotificationRepository import com.example.notimanager.data.repository.NotificationDomainRepository -import com.example.notimanager.data.repository.NotificationRepositoryInterface +import com.example.notimanager.domain.repository.NotificationRepositoryInterface import com.example.notimanager.data.repository.NotificationTitleRepository import com.example.notimanager.data.source.local.dao.AppIconDao import com.example.notimanager.data.source.local.dao.NotificationDao diff --git a/app/src/main/java/com/example/notimanager/data/repository/NotificationRepositoryInterface.kt b/app/src/main/java/com/example/notimanager/domain/repository/NotificationRepositoryInterface.kt similarity index 92% rename from app/src/main/java/com/example/notimanager/data/repository/NotificationRepositoryInterface.kt rename to app/src/main/java/com/example/notimanager/domain/repository/NotificationRepositoryInterface.kt index 6983167..93133bb 100644 --- a/app/src/main/java/com/example/notimanager/data/repository/NotificationRepositoryInterface.kt +++ b/app/src/main/java/com/example/notimanager/domain/repository/NotificationRepositoryInterface.kt @@ -1,4 +1,4 @@ -package com.example.notimanager.data.repository +package com.example.notimanager.domain.repository import com.example.notimanager.data.model.AppIconModel import com.example.notimanager.data.model.NotificationIconModel diff --git a/app/src/main/java/com/example/notimanager/data/service/NotiListenerService.kt b/app/src/main/java/com/example/notimanager/domain/service/NotiListenerService.kt similarity index 96% rename from app/src/main/java/com/example/notimanager/data/service/NotiListenerService.kt rename to app/src/main/java/com/example/notimanager/domain/service/NotiListenerService.kt index df49b25..76e9653 100644 --- a/app/src/main/java/com/example/notimanager/data/service/NotiListenerService.kt +++ b/app/src/main/java/com/example/notimanager/domain/service/NotiListenerService.kt @@ -1,14 +1,13 @@ -package com.example.notimanager.data.service +package com.example.notimanager.domain.service import android.graphics.drawable.Icon import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification -import android.util.Log import com.example.notimanager.data.model.AppIconModel import com.example.notimanager.data.model.NotificationIconModel import com.example.notimanager.data.model.NotificationMetaModel import com.example.notimanager.data.model.NotificationModel -import com.example.notimanager.data.repository.NotificationRepositoryInterface +import com.example.notimanager.domain.repository.NotificationRepositoryInterface import com.example.notimanager.data.utils.AppIconGetter.convertByteArray import com.example.notimanager.data.utils.AppIconGetter.convertByteArrayWithColor import com.example.notimanager.data.utils.NameGetter diff --git a/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationAppListView.kt b/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationAppListView.kt index 1f16ac3..55813e9 100644 --- a/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationAppListView.kt +++ b/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationAppListView.kt @@ -5,6 +5,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -19,6 +20,7 @@ import androidx.compose.material3.IconButton 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.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -43,52 +45,59 @@ import com.example.notimanager.presentation.stateholder.viewmodel.NotificationAp @Composable fun NotificationAppListView( navController: NavController, - innerPadding: PaddingValues, - viewModel: NotificationAppViewModel = hiltViewModel(), - priorityViewModel: NotificationAppPriorityViewModel = hiltViewModel() + viewModel: NotificationAppViewModel, + priorityViewModel: NotificationAppPriorityViewModel ) { val notificationAppState by viewModel.notificationAppState.observeAsState(NotificationAppState()) val priorityState by priorityViewModel.notificationAppPriorityState.observeAsState((NotificationAppPriorityState())) + var currentNotiPriority by remember { mutableStateOf(priorityState.notificationAppList) } + var currentNoti by remember { mutableStateOf(notificationAppState.notificationAppList) } - Column( - Modifier.padding(innerPadding) - ) { - if (notificationAppState.isLoading || priorityState.isLoading) { - CircularProgressIndicator(modifier = Modifier.padding(16.dp)) - } else if (notificationAppState.error != null) { + LaunchedEffect(priorityState.notificationAppList) { + if (!priorityState.isLoading) { + currentNotiPriority = priorityState.notificationAppList + } + } - } else { - LazyColumn { - items(priorityState.notificationAppList) { notification -> - NotificationAppItemView( - notification = notification, - onClick = { - navController - .navigate("titleScreen/${notification.appName}/${notification.title}" - ) - }, - viewModel = viewModel, - priorityViewModel = priorityViewModel - ) - } + LaunchedEffect(notificationAppState.notificationAppList) { + if (!notificationAppState.isLoading) { + currentNoti = notificationAppState.notificationAppList + } + } - item { - HorizontalDivider() - } + LazyColumn( + Modifier.fillMaxSize() + ) { + items(currentNotiPriority) { notification -> + NotificationAppItemView( + notification = notification, + onClick = { + navController + .navigate( + "titleScreen/${notification.appName}/${notification.title}" + ) + }, + viewModel = viewModel, + priorityViewModel = priorityViewModel + ) + } - items(notificationAppState.notificationAppList) { notification -> - NotificationAppItemView( - notification = notification, - onClick = { - navController - .navigate("titleScreen/${notification.appName}/${notification.title}" - ) - }, - viewModel = viewModel, - priorityViewModel = priorityViewModel - ) - } - } + item { + HorizontalDivider() + } + + items(currentNoti) { notification -> + NotificationAppItemView( + notification = notification, + onClick = { + navController + .navigate( + "titleScreen/${notification.appName}/${notification.title}" + ) + }, + viewModel = viewModel, + priorityViewModel = priorityViewModel + ) } } } @@ -157,7 +166,7 @@ fun NotificationAppItemView( priorityViewModel.removeAppPriority(notification.appName){ viewModel.loadNotificationApps() } - + showModal = false }) } else{ @@ -165,6 +174,7 @@ fun NotificationAppItemView( viewModel.setAppPriority(notification.appName, priorityViewModel.getLength()){ priorityViewModel.loadNotificationAppPriority() } + showModal = false }) } ClickableTextView(text = "삭제", onClick = {}) diff --git a/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationTitleListView.kt b/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationTitleListView.kt index cce4e19..899cd92 100644 --- a/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationTitleListView.kt +++ b/app/src/main/java/com/example/notimanager/presentation/ui/component/NotificationTitleListView.kt @@ -5,6 +5,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -19,6 +20,7 @@ import androidx.compose.material3.IconButton 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.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -41,7 +43,6 @@ import com.example.notimanager.presentation.stateholder.viewmodel.NotificationTi @Composable fun NotificationTitleListView( navController: NavController, - innerPadding: PaddingValues, viewModel: NotificationTitleViewModel, priorityViewModel: NotificationTitlePriorityViewModel ) { @@ -51,35 +52,42 @@ fun NotificationTitleListView( val priorityState by priorityViewModel.notificationTitlePriorityState.observeAsState( NotificationTitlePriorityState() ) - Column ( - Modifier.padding(innerPadding) - ){ - if (notificationTitleState.isLoading || priorityState.isLoading) { - CircularProgressIndicator(modifier = Modifier.padding(16.dp)) - } else if (notificationTitleState.error != null) { + var currentNotiPriority by remember { mutableStateOf(priorityState.notificationTitleList) } + var currentNoti by remember { mutableStateOf(notificationTitleState.notificationTitleList) } - } else { - LazyColumn { - items(priorityState.notificationTitleList) { notification -> - NotificationTitleItemView(notification = notification, onClick = { - navController.navigate( - "notificationScreen/${viewModel.getAppName()}/${notification.title}" - ) - }, viewModel = viewModel, priorityViewModel = priorityViewModel) - } + LaunchedEffect(priorityState.notificationTitleList) { + if (!priorityState.isLoading) { + currentNotiPriority = priorityState.notificationTitleList + } + } - item { - HorizontalDivider() - } + LaunchedEffect(notificationTitleState.notificationTitleList) { + if (!notificationTitleState.isLoading) { + currentNoti = notificationTitleState.notificationTitleList + } + } - items(notificationTitleState.notificationTitleList) { notification -> - NotificationTitleItemView(notification = notification, onClick = { - navController.navigate( - "notificationScreen/${viewModel.getAppName()}/${notification.title}" - ) - }, viewModel = viewModel, priorityViewModel = priorityViewModel) - } - } + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(currentNotiPriority) { notification -> + NotificationTitleItemView(notification = notification, onClick = { + navController.navigate( + "notificationScreen/${viewModel.getAppName()}/${notification.title}" + ) + }, viewModel = viewModel, priorityViewModel = priorityViewModel) + } + + item { + HorizontalDivider() + } + + items(currentNoti) { notification -> + NotificationTitleItemView(notification = notification, onClick = { + navController.navigate( + "notificationScreen/${viewModel.getAppName()}/${notification.title}" + ) + }, viewModel = viewModel, priorityViewModel = priorityViewModel) } } } diff --git a/app/src/main/java/com/example/notimanager/presentation/ui/screen/MainScreen.kt b/app/src/main/java/com/example/notimanager/presentation/ui/screen/MainScreen.kt index f6cf750..166a1d0 100644 --- a/app/src/main/java/com/example/notimanager/presentation/ui/screen/MainScreen.kt +++ b/app/src/main/java/com/example/notimanager/presentation/ui/screen/MainScreen.kt @@ -1,18 +1,58 @@ package com.example.notimanager.presentation.ui.screen +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.navigation.NavController +import com.example.notimanager.presentation.stateholder.viewmodel.NotificationAppPriorityViewModel +import com.example.notimanager.presentation.stateholder.viewmodel.NotificationAppViewModel import com.example.notimanager.presentation.ui.component.MainTopAppBar import com.example.notimanager.presentation.ui.component.NotificationAppListView import com.example.notimanager.presentation.ui.component.PermissionCheck +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainScreen(navController: NavController){ +fun MainScreen( + navController: NavController, +) { + val viewModel: NotificationAppViewModel = hiltViewModel() + val priorityViewModel: NotificationAppPriorityViewModel = hiltViewModel() + + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + val onRefresh: () -> Unit = { + isRefreshing = true + viewModel.loadNotificationApps() + priorityViewModel.loadNotificationAppPriority() + coroutineScope.launch { + delay(500) + isRefreshing = false + } + } + + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + viewModel.loadNotificationApps() + priorityViewModel.loadNotificationAppPriority() + } + Scaffold( topBar = { MainTopAppBar() @@ -22,8 +62,15 @@ fun MainScreen(navController: NavController){ modifier = Modifier.padding(innerPadding), thickness = 0.2.dp ) - NotificationAppListView(navController, innerPadding) + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) { + NotificationAppListView(navController, viewModel, priorityViewModel) + } } PermissionCheck() - } \ No newline at end of file diff --git a/app/src/main/java/com/example/notimanager/presentation/ui/screen/TitleScreen.kt b/app/src/main/java/com/example/notimanager/presentation/ui/screen/TitleScreen.kt index 9d6091b..f666a5e 100644 --- a/app/src/main/java/com/example/notimanager/presentation/ui/screen/TitleScreen.kt +++ b/app/src/main/java/com/example/notimanager/presentation/ui/screen/TitleScreen.kt @@ -1,28 +1,50 @@ package com.example.notimanager.presentation.ui.screen import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.navigation.NavController import com.example.notimanager.presentation.stateholder.viewmodel.NotificationTitlePriorityViewModel import com.example.notimanager.presentation.stateholder.viewmodel.NotificationTitleViewModel +import com.example.notimanager.presentation.ui.component.NotificationAppListView import com.example.notimanager.presentation.ui.component.NotificationTitleListView import com.example.notimanager.presentation.ui.component.TitleTopAppBar +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TitleScreen(navController: NavController, appName: String = "", title:String = ""){ val viewModel: NotificationTitleViewModel = hiltViewModel() val priorityViewModel: NotificationTitlePriorityViewModel = hiltViewModel() + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(appName, title) { viewModel.setArgs(appName, title) priorityViewModel.setArgs(appName, title) } + + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + viewModel.loadNotificationTitles() + priorityViewModel.loadNotificationTitles() + } + Scaffold( topBar = { TitleTopAppBar(title = title, onBackClick = { navController.popBackStack() }) @@ -32,6 +54,22 @@ fun TitleScreen(navController: NavController, appName: String = "", title:String modifier = Modifier.padding(innerPadding), thickness = 0.2.dp ) - NotificationTitleListView(navController, innerPadding, viewModel, priorityViewModel) + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadNotificationTitles() + priorityViewModel.loadNotificationTitles() + coroutineScope.launch { + delay(500) + isRefreshing = false + } + }, + modifier = Modifier.padding(innerPadding) + ) { + NotificationTitleListView(navController, viewModel, priorityViewModel) + } + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa93334..e3d1df0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.8.1" +agp = "8.8.2" byteBuddy = "1.17.1" byteBuddyAgent = "1.17.1" coilCompose = "3.1.0" @@ -22,6 +22,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinStdlib = "2.0.21" kotlinxCoroutinesAndroid = "1.8.0" +lifecycleRuntimeCompose = "2.8.7" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.10.0" composeBom = "2025.02.00" @@ -40,6 +41,7 @@ runtimeLivedata = "1.7.8" androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } androidx-room-ktx = { module = "androidx.room:room-ktx" }