From aa12de428af9aaa1392f65af897a4115f655fd68 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:56:08 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[setting#5]=20=EC=95=B1=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=ED=8F=AC=EA=B7=B8=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Android=20=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `AndroidManifest.xml`에 앱 사용 기록 접근(`PACKAGE_USAGE_STATS`), 시스템 오버레이(`SYSTEM_ALERT_WINDOW`), 배터리 최적화 제외(`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`) 권한을 추가하였습니다. - 알림 표시(`POST_NOTIFICATIONS`) 및 포그라운드 서비스(`FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_SPECIAL_USE`) 관련 권한을 추가하였습니다. - 인터넷 연결(`INTERNET`) 및 전체 패키지 조회(`QUERY_ALL_PACKAGES`) 권한을 설정하였습니다. --- app/src/main/AndroidManifest.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fdd4d5a..7786d59 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,20 @@ + + + + + + + + + + + Date: Tue, 12 May 2026 17:56:29 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[Feat#5]=20UrlImage=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20Coil=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `core:designsystem` 모듈에 URL을 통해 이미지를 로드하는 공통 컴포넌트인 `UrlImage`를 추가하였습니다. - `UrlImage` 컴포넌트 구현을 위해 `coil-compose` 의존성을 `build.gradle.kts`에 추가하고 `AsyncImage`를 활용하도록 개발하였습니다. - Compose Preview 환경에서 이미지 영역을 확인할 수 있도록 `LocalInspectionMode`일 때 표시될 플레이스홀더 이미지(`img_fake_red.xml`)를 추가하였습니다. - `UrlImage`의 기본 `ContentScale` 설정을 `Fit`으로 지정하고, 외부에서 `Modifier` 및 설명을 주입받을 수 있도록 구성하였습니다. --- core/designsystem/build.gradle.kts | 4 ++ .../core/designsystem/component/UrlImage.kt | 47 +++++++++++++++++++ .../src/main/res/drawable/img_fake_red.xml | 10 ++++ 3 files changed, 61 insertions(+) create mode 100644 core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt create mode 100644 core/designsystem/src/main/res/drawable/img_fake_red.xml diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index bdbbc2f..9ba2e76 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -8,3 +8,7 @@ plugins { android { setNamespace("core.designsystem") } + +dependencies { + implementation(libs.coil.compose) +} diff --git a/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt b/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt new file mode 100644 index 0000000..3fbfba6 --- /dev/null +++ b/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt @@ -0,0 +1,47 @@ +package com.kindl.core.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.kindl.core.designsystem.R + +@Composable +fun UrlImage( + url: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, + contentDescription: String? = null, +) { + if (LocalInspectionMode.current) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.img_fake_red), + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } else { + AsyncImage( + model = url, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } +} + +@Preview +@Composable +fun UrlImagePreview() { + UrlImage( + url = "", + modifier = Modifier.size(100.dp), + ) +} diff --git a/core/designsystem/src/main/res/drawable/img_fake_red.xml b/core/designsystem/src/main/res/drawable/img_fake_red.xml new file mode 100644 index 0000000..abebdb2 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/img_fake_red.xml @@ -0,0 +1,10 @@ + + + + From 5d49856662d1f42d7aa99f47f43bafa754f8dd3c Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:56:54 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[feat#5]=20core:permission=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `core:permission` 모듈을 신규 생성하고 관련 빌드 설정(`build.gradle.kts`) 및 `AndroidManifest.xml`을 추가하였습니다. - 권한 상태 확인 및 설정 페이지 이동을 담당하는 `PermissionManager` 인터페이스와 그 구현체인 `PermissionManagerImpl`을 작성하였습니다. - `PermissionType` 열거형을 통해 USAGE_STATS, OVERLAY, BATTERY_OPTIMIZATION, POST_NOTIFICATIONS 등 관리 대상을 정의하였습니다. - 각 권한별 실제 부여 여부를 체크하는 `PermissionChecker` 로직(AppOpsManager, Settings.canDrawOverlays 등 활용)을 구현하였습니다. - 권한 관련 비즈니스 로직 처리를 위한 UseCase(`AreAllFocusPermissionsGrantedUseCase`, `CheckFocusPermissionsUseCase`)를 추가하였습니다. - Hilt를 사용하여 `PermissionManager` 의존성 주입을 위한 `PermissionModule`을 구성하였습니다. --- core/permission/build.gradle.kts | 10 ++++ core/permission/src/main/AndroidManifest.xml | 2 + .../core/permission/PermissionChecker.kt | 59 +++++++++++++++++++ .../core/permission/PermissionManager.kt | 10 ++++ .../core/permission/PermissionManagerImpl.kt | 59 +++++++++++++++++++ .../kindl/core/permission/PermissionType.kt | 8 +++ .../core/permission/di/PermissionModule.kt | 18 ++++++ .../AreAllFocusPermissionsGrantedUseCase.kt | 11 ++++ .../usecase/CheckFocusPermissionsUseCase.kt | 12 ++++ 9 files changed, 189 insertions(+) create mode 100644 core/permission/build.gradle.kts create mode 100644 core/permission/src/main/AndroidManifest.xml create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/di/PermissionModule.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/usecase/AreAllFocusPermissionsGrantedUseCase.kt create mode 100644 core/permission/src/main/kotlin/com/kindl/core/permission/usecase/CheckFocusPermissionsUseCase.kt diff --git a/core/permission/build.gradle.kts b/core/permission/build.gradle.kts new file mode 100644 index 0000000..217d741 --- /dev/null +++ b/core/permission/build.gradle.kts @@ -0,0 +1,10 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.library) + alias(libs.plugins.kindl.hilt) +} + +android { + setNamespace("core.permission") +} \ No newline at end of file diff --git a/core/permission/src/main/AndroidManifest.xml b/core/permission/src/main/AndroidManifest.xml new file mode 100644 index 0000000..568741e --- /dev/null +++ b/core/permission/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt new file mode 100644 index 0000000..ad32976 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt @@ -0,0 +1,59 @@ +package com.kindl.core.permission + +import android.Manifest +import android.annotation.SuppressLint +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.PowerManager +import android.os.Process +import android.provider.Settings + +internal fun interface PermissionChecker { + fun isGranted(context: Context): Boolean + + object UsageStats : PermissionChecker { + override fun isGranted(context: Context): Boolean { + val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + @Suppress("DEPRECATION") + appOps.unsafeCheckOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + Process.myUid(), + context.packageName, + ) + } else { + appOps.checkOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + Process.myUid(), + context.packageName, + ) + } + return mode == AppOpsManager.MODE_ALLOWED + } + } + + object Overlay : PermissionChecker { + override fun isGranted(context: Context): Boolean = + Settings.canDrawOverlays(context) + } + + object BatteryOptimization : PermissionChecker { + @SuppressLint("ServiceCast") + override fun isGranted(context: Context): Boolean { + val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(context.packageName) + } + } + + object PostNotifications : PermissionChecker { + override fun isGranted(context: Context): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } else { + true + } + } +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt new file mode 100644 index 0000000..edaed58 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt @@ -0,0 +1,10 @@ +package com.kindl.core.permission + +import android.content.Intent + +interface PermissionManager { + fun check(type: PermissionType): Boolean + fun allFocusPermissionsGranted(): Boolean + fun missingFocusPermissions(): List + fun openSettings(type: PermissionType) : Intent +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt new file mode 100644 index 0000000..64a3b73 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt @@ -0,0 +1,59 @@ +package com.kindl.core.permission + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton +import androidx.core.net.toUri + +@Singleton +internal class PermissionManagerImpl @Inject constructor( + @param:ApplicationContext private val context: Context, +) : PermissionManager { + + private val checkers: Map = mapOf( + PermissionType.USAGE_STATS to PermissionChecker.UsageStats, + PermissionType.OVERLAY to PermissionChecker.Overlay, + PermissionType.BATTERY_OPTIMIZATION to PermissionChecker.BatteryOptimization, + PermissionType.POST_NOTIFICATIONS to PermissionChecker.PostNotifications, + ) + + private val focusRequiredPermissions = + PermissionType.entries.filter { it.isFocusRequired } + + override fun check(type: PermissionType): Boolean = + checkers[type]?.isGranted(context) ?: false + + override fun allFocusPermissionsGranted(): Boolean = + focusRequiredPermissions.all { check(it) } + + override fun missingFocusPermissions(): List = + focusRequiredPermissions.filter { !check(it) } + + // intent만 반환하고 실제 이동은 ui 레이어에서 + override fun openSettings(type: PermissionType) : Intent { + val packageUri = "package:${context.packageName}".toUri() + val intent = when (type) { + PermissionType.USAGE_STATS -> + Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) + + PermissionType.OVERLAY -> + Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, packageUri) + + PermissionType.BATTERY_OPTIMIZATION -> + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, packageUri) + + PermissionType.POST_NOTIFICATIONS -> + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + }.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + return intent + } +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt new file mode 100644 index 0000000..4a10421 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt @@ -0,0 +1,8 @@ +package com.kindl.core.permission + +enum class PermissionType(val isFocusRequired: Boolean) { + USAGE_STATS(true), + OVERLAY(true), + BATTERY_OPTIMIZATION(true), + POST_NOTIFICATIONS(true), +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/di/PermissionModule.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/di/PermissionModule.kt new file mode 100644 index 0000000..991d3e2 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/di/PermissionModule.kt @@ -0,0 +1,18 @@ +package com.kindl.core.permission.di + +import com.kindl.core.permission.PermissionManager +import com.kindl.core.permission.PermissionManagerImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class PermissionModule { + + @Binds + @Singleton + abstract fun bindPermissionManager(impl: PermissionManagerImpl): PermissionManager +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/AreAllFocusPermissionsGrantedUseCase.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/AreAllFocusPermissionsGrantedUseCase.kt new file mode 100644 index 0000000..26b6600 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/AreAllFocusPermissionsGrantedUseCase.kt @@ -0,0 +1,11 @@ +package com.kindl.core.permission.usecase + +import com.kindl.core.permission.PermissionManager +import javax.inject.Inject + +class AreAllFocusPermissionsGrantedUseCase @Inject constructor( + private val permissionManager: PermissionManager, +) { + operator fun invoke(): Boolean = + permissionManager.allFocusPermissionsGranted() +} diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/CheckFocusPermissionsUseCase.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/CheckFocusPermissionsUseCase.kt new file mode 100644 index 0000000..d998557 --- /dev/null +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/usecase/CheckFocusPermissionsUseCase.kt @@ -0,0 +1,12 @@ +package com.kindl.core.permission.usecase + +import com.kindl.core.permission.PermissionManager +import com.kindl.core.permission.PermissionType +import javax.inject.Inject + +class CheckFocusPermissionsUseCase @Inject constructor( + private val permissionManager: PermissionManager, +) { + operator fun invoke(): List = + permissionManager.missingFocusPermissions() +} From b0efbb27f43faded8000129f6fb9ac4372807065 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:57:14 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[Add#5]=20core:service=20=EB=B0=8F=20core?= =?UTF-8?q?:work=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `core:service` 모듈을 생성하고 `AndroidManifest.xml`, `build.gradle.kts`, `.gitignore` 등 기본 설정을 완료하였습니다. - `core:service` 모듈에 `android-library` 및 `android-compose` 컨벤션 플러그인을 적용하였습니다. - `core:work` 모듈을 생성하고 `AndroidManifest.xml`, `build.gradle.kts`, `.gitignore` 등 기본 설정을 완료하였습니다. - `core:work` 모듈에 `android-library` 및 `hilt` 컨벤션 플러그인을 적용하였습니다. - 각 모듈의 네임스페이스를 `core.service` 및 `core.work`로 설정하였습니다. --- {presentation => core/service}/.gitignore | 0 core/service/build.gradle.kts | 10 ++++++++++ core/service/src/main/AndroidManifest.xml | 4 ++++ core/work/.gitignore | 1 + core/work/build.gradle.kts | 10 ++++++++++ core/work/src/main/AndroidManifest.xml | 4 ++++ 6 files changed, 29 insertions(+) rename {presentation => core/service}/.gitignore (100%) create mode 100644 core/service/build.gradle.kts create mode 100644 core/service/src/main/AndroidManifest.xml create mode 100644 core/work/.gitignore create mode 100644 core/work/build.gradle.kts create mode 100644 core/work/src/main/AndroidManifest.xml diff --git a/presentation/.gitignore b/core/service/.gitignore similarity index 100% rename from presentation/.gitignore rename to core/service/.gitignore diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts new file mode 100644 index 0000000..1f7eb63 --- /dev/null +++ b/core/service/build.gradle.kts @@ -0,0 +1,10 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.library) + alias(libs.plugins.kindl.android.compose) +} + +android { + setNamespace("core.service") +} diff --git a/core/service/src/main/AndroidManifest.xml b/core/service/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/service/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/work/.gitignore b/core/work/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/work/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/work/build.gradle.kts b/core/work/build.gradle.kts new file mode 100644 index 0000000..f2909d7 --- /dev/null +++ b/core/work/build.gradle.kts @@ -0,0 +1,10 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.library) + alias(libs.plugins.kindl.hilt) +} + +android { + setNamespace("core.work") +} diff --git a/core/work/src/main/AndroidManifest.xml b/core/work/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/work/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + From ce404fd775cee0dc10e7c0d46e896dbd8eac4353 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:57:34 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[Feat#5]=20data:forbidden=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=EB=90=9C=20=EC=95=B1=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `data:forbidden` 모듈을 신규 생성하고 `build.gradle.kts` 및 기본 프로젝트 설정을 완료하였습니다. - `ForbiddenAppRepositoryImpl`을 구현하여 기기에 설치된 사용자 앱 목록을 가져오는 기능을 추가하였습니다. - `PackageManager`를 사용하여 `CATEGORY_LAUNCHER` 속성을 가진 앱들을 필터링하고 이름순으로 정렬하도록 구현하였습니다. - Hilt를 이용한 의존성 주입을 위해 `ForbiddenRepositoryModule`을 추가하고 `ForbiddenAppRepository`를 바인딩하였습니다. - `domain:forbidden` 모듈에 대한 의존성을 추가하였습니다. --- data/forbidden/.gitignore | 1 + data/forbidden/build.gradle.kts | 13 ++++++++ data/forbidden/consumer-rules.pro | 0 data/forbidden/proguard-rules.pro | 21 +++++++++++++ data/forbidden/src/main/AndroidManifest.xml | 4 +++ .../forbidden/di/ForbiddenRepositoryModule.kt | 19 ++++++++++++ .../ForbiddenAppRepositoryImpl.kt | 31 +++++++++++++++++++ 7 files changed, 89 insertions(+) create mode 100644 data/forbidden/.gitignore create mode 100644 data/forbidden/build.gradle.kts create mode 100644 data/forbidden/consumer-rules.pro create mode 100644 data/forbidden/proguard-rules.pro create mode 100644 data/forbidden/src/main/AndroidManifest.xml create mode 100644 data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt create mode 100644 data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt diff --git a/data/forbidden/.gitignore b/data/forbidden/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/data/forbidden/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/forbidden/build.gradle.kts b/data/forbidden/build.gradle.kts new file mode 100644 index 0000000..eedf00b --- /dev/null +++ b/data/forbidden/build.gradle.kts @@ -0,0 +1,13 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.data) +} + +android { + setNamespace("data.forbidden") +} + +dependencies { + implementation(projects.domain.forbidden) +} diff --git a/data/forbidden/consumer-rules.pro b/data/forbidden/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/forbidden/proguard-rules.pro b/data/forbidden/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/data/forbidden/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/data/forbidden/src/main/AndroidManifest.xml b/data/forbidden/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/data/forbidden/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt new file mode 100644 index 0000000..2d8986e --- /dev/null +++ b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt @@ -0,0 +1,19 @@ +package com.kindl.data.forbidden.di + +import com.kindl.data.forbidden.repositoryimpl.ForbiddenAppRepositoryImpl +import com.kindl.domain.forbidden.repository.ForbiddenAppRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface ForbiddenRepositoryModule { + @Binds + @Singleton + fun bindForbiddenRepository( + impl: ForbiddenAppRepositoryImpl + ) : ForbiddenAppRepository +} diff --git a/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt b/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt new file mode 100644 index 0000000..5af0d7f --- /dev/null +++ b/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt @@ -0,0 +1,31 @@ +package com.kindl.data.forbidden.repositoryimpl + +import android.content.Context +import android.content.Intent +import com.kindl.domain.forbidden.model.InstalledAppModel +import com.kindl.domain.forbidden.repository.ForbiddenAppRepository +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class ForbiddenAppRepositoryImpl @Inject constructor( + @param:ApplicationContext private val context: Context, +) : ForbiddenAppRepository { + override fun getInstalledUserApps(): List { + val pm = context.packageManager + + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_LAUNCHER) + } + + return pm.queryIntentActivities(intent, 0) + .filter { it.activityInfo.packageName != context.packageName } + .map { resolveInfo -> + InstalledAppModel( + packageName = resolveInfo.activityInfo.packageName, + appName = resolveInfo.loadLabel(pm).toString(), + //icon = resolveInfo.loadIcon(pm), + ) + } + .sortedBy { it.appName } + } +} From da547f0deea839b77b64ee7b97a58567505145fe Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:57:42 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[Feat#5]=20`domain:forbidden`=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=EB=90=9C=20=EC=95=B1=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `presentation/build.gradle.kts`를 `domain/forbidden/build.gradle.kts`로 이동하고, `kindl.kotlin.jvm` 컨벤션 플러그인 및 `javax.inject` 의존성을 적용하였습니다. - 설치된 앱 정보를 담는 `InstalledAppModel` 데이터 클래스를 추가하였습니다. - 앱 목록 조회를 위한 `ForbiddenAppRepository` 인터페이스를 정의하였습니다. - 의존성 주입(`@Inject`)을 지원하는 `GetInstalledAppsUseCase`를 구현하여 비즈니스 로직을 추가하였습니다. - `domain/forbidden` 모듈에 대한 `.gitignore` 설정을 추가하였습니다. --- domain/forbidden/.gitignore | 1 + domain/forbidden/build.gradle.kts | 7 +++++++ .../domain/forbidden/model/InstalledAppModel.kt | 6 ++++++ .../forbidden/repository/ForbiddenAppRepository.kt | 7 +++++++ .../forbidden/usecase/GetInstalledAppsUseCase.kt | 12 ++++++++++++ presentation/build.gradle.kts | 13 ------------- 6 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 domain/forbidden/.gitignore create mode 100644 domain/forbidden/build.gradle.kts create mode 100644 domain/forbidden/src/main/java/com/kindl/domain/forbidden/model/InstalledAppModel.kt create mode 100644 domain/forbidden/src/main/java/com/kindl/domain/forbidden/repository/ForbiddenAppRepository.kt create mode 100644 domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt delete mode 100644 presentation/build.gradle.kts diff --git a/domain/forbidden/.gitignore b/domain/forbidden/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/domain/forbidden/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/forbidden/build.gradle.kts b/domain/forbidden/build.gradle.kts new file mode 100644 index 0000000..0856fec --- /dev/null +++ b/domain/forbidden/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.kindl.kotlin.jvm) +} + +dependencies { + implementation(libs.javax.inject) +} diff --git a/domain/forbidden/src/main/java/com/kindl/domain/forbidden/model/InstalledAppModel.kt b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/model/InstalledAppModel.kt new file mode 100644 index 0000000..6a7f675 --- /dev/null +++ b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/model/InstalledAppModel.kt @@ -0,0 +1,6 @@ +package com.kindl.domain.forbidden.model + +data class InstalledAppModel( + val packageName: String, + val appName: String, +) diff --git a/domain/forbidden/src/main/java/com/kindl/domain/forbidden/repository/ForbiddenAppRepository.kt b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/repository/ForbiddenAppRepository.kt new file mode 100644 index 0000000..c7159c4 --- /dev/null +++ b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/repository/ForbiddenAppRepository.kt @@ -0,0 +1,7 @@ +package com.kindl.domain.forbidden.repository + +import com.kindl.domain.forbidden.model.InstalledAppModel + +interface ForbiddenAppRepository { + fun getInstalledUserApps(): List +} diff --git a/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt new file mode 100644 index 0000000..a57c7fa --- /dev/null +++ b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt @@ -0,0 +1,12 @@ +package com.kindl.domain.forbidden.usecase + +import com.kindl.domain.forbidden.model.InstalledAppModel +import com.kindl.domain.forbidden.repository.ForbiddenAppRepository +import javax.inject.Inject + +class GetInstalledAppsUseCase @Inject constructor( + private val appRepository: ForbiddenAppRepository, +) { + operator fun invoke(): List = + appRepository.getInstalledUserApps() +} diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts deleted file mode 100644 index f3bd531..0000000 --- a/presentation/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("java-library") - alias(libs.plugins.jetbrains.kotlin.jvm) -} -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} -kotlin { - compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 - } -} From e7ab4209273501678123e87650a64cc777447859 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:58:11 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[Feat#5]=20presentation:home=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98/=EA=B6=8C=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `presentation:home` 모듈을 신규 생성하고 기본 빌드 설정 및 ProGuard 규칙을 추가하였습니다. - `presentation:main` 모듈에 `MainBottomBar` 컴포넌트를 추가하고 `MainTab`에 홈 항목을 정의하였습니다. - `MainViewModel`, `MainUiState`, `MainSideEffect`를 추가하여 권한 체크 및 설정 이동 로직을 구현하였습니다. - `MainScreen`에서 `Scaffold`와 `NavHost`를 활용하여 바텀바 표시 및 화면 전환 애니메이션(Slide) 설정을 추가하였습니다. - `MainAppState`에 기본 시작 목적지(`Splash`)를 설정하고, `core:permission` 의존성을 추가하였습니다. --- presentation/home/.gitignore | 1 + presentation/home/build.gradle.kts | 13 +++ presentation/home/consumer-rules.pro | 0 presentation/home/proguard-rules.pro | 21 ++++ .../home/src/main/AndroidManifest.xml | 4 + presentation/main/build.gradle.kts | 2 + .../com/kindl/presentation/main/MainScreen.kt | 57 ++++++++++ .../com/kindl/presentation/main/MainTab.kt | 6 +- .../kindl/presentation/main/MainViewModel.kt | 48 ++++++++ .../main/component/MainBottomBar.kt | 107 ++++++++++++++++++ .../presentation/main/state/MainAppState.kt | 4 +- .../presentation/main/state/MainUiState.kt | 15 +++ 12 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 presentation/home/.gitignore create mode 100644 presentation/home/build.gradle.kts create mode 100644 presentation/home/consumer-rules.pro create mode 100644 presentation/home/proguard-rules.pro create mode 100644 presentation/home/src/main/AndroidManifest.xml create mode 100644 presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt create mode 100644 presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt create mode 100644 presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt diff --git a/presentation/home/.gitignore b/presentation/home/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/presentation/home/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/presentation/home/build.gradle.kts b/presentation/home/build.gradle.kts new file mode 100644 index 0000000..7c39916 --- /dev/null +++ b/presentation/home/build.gradle.kts @@ -0,0 +1,13 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.presentation) +} + +android { + setNamespace("presentation.home") +} + +dependencies { + +} diff --git a/presentation/home/consumer-rules.pro b/presentation/home/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/presentation/home/proguard-rules.pro b/presentation/home/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/presentation/home/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/presentation/home/src/main/AndroidManifest.xml b/presentation/home/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/presentation/home/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/presentation/main/build.gradle.kts b/presentation/main/build.gradle.kts index 332f3fe..424778a 100644 --- a/presentation/main/build.gradle.kts +++ b/presentation/main/build.gradle.kts @@ -13,4 +13,6 @@ android { } dependencies { + //core + implementation(projects.core.permission) } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt index 5607de0..2277841 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt @@ -1,6 +1,12 @@ package com.kindl.presentation.main +import MainViewModel import android.widget.Toast +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -9,11 +15,16 @@ 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.platform.LocalContext +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.compose.NavHost import com.kindl.core.common.model.DialogTrigger import com.kindl.core.common.model.GlobalUiEventHolder import com.kindl.core.common.model.SnackbarState import com.kindl.core.common.trigger.LocalGlobalUiEventTrigger +import com.kindl.presentation.main.component.MainBottomBar import com.kindl.presentation.main.state.MainAppState import com.kindl.presentation.main.state.rememberDialogStateHolder import com.kindl.presentation.main.state.rememberMainAppState @@ -23,11 +34,15 @@ import kotlinx.coroutines.launch @Composable internal fun MainScreen( appState: MainAppState = rememberMainAppState(), + viewModel: MainViewModel = hiltViewModel(), ) { val context = LocalContext.current val scope = rememberCoroutineScope() + val isBottomBarVisible by appState.isBottomBarVisible.collectAsStateWithLifecycle() + val currentTab by appState.currentTab.collectAsStateWithLifecycle() val dialogState = rememberDialogStateHolder() + val state by viewModel.state.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } var currentSnackbarState by remember { mutableStateOf(null) } @@ -78,6 +93,48 @@ internal fun MainScreen( CompositionLocalProvider( LocalGlobalUiEventTrigger provides eventHolder, ) { + Scaffold ( + bottomBar = { + if (isBottomBarVisible == true) { + MainBottomBar( + currentTab = currentTab, + onTabSelected = appState::navigate, + ) + } + }, + modifier = Modifier + .fillMaxSize() + ) { paddingValues -> + NavHost( + navController = appState.navController, + startDestination = appState.startDestination, + enterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + popEnterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + popExitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + ) { + } + } } } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt index ce1e3a0..83a54c7 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt @@ -9,7 +9,11 @@ internal enum class MainTab( val route: MainTabRoute, val label: String, ) { - ; + HOME( + iconRes = com.kindl.core.designsystem.R.drawable.ic_home, + route = MainTabRoute.HOME, + label = "홈", + ); companion object { fun find(predicate: (MainTabRoute) -> Boolean): MainTab? = entries.find { predicate(it.route) } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt new file mode 100644 index 0000000..1387dbb --- /dev/null +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt @@ -0,0 +1,48 @@ +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kindl.core.permission.PermissionManager +import com.kindl.core.permission.PermissionType +import com.kindl.core.permission.usecase.CheckFocusPermissionsUseCase +import com.kindl.presentation.main.state.MainSideEffect +import com.kindl.presentation.main.state.MainUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + private val checkFocusPermissions: CheckFocusPermissionsUseCase, + private val permissionManager: PermissionManager, +) : ViewModel() { + + private val _state = MutableStateFlow(MainUiState()) + val state: StateFlow = _state.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffect.asSharedFlow() + + init { + checkPermissions() + } + + fun checkPermissions() { + val missingPermissions = checkFocusPermissions() + _state.update { + it.copy( + missingPermissions = missingPermissions, + isPermissionCheckComplete = true, + ) + } + } + + fun openSettings(type: PermissionType) { + permissionManager.openSettings(type) + } +} diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt b/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt new file mode 100644 index 0000000..b502206 --- /dev/null +++ b/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt @@ -0,0 +1,107 @@ +package com.kindl.presentation.main.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kindl.core.common.extension.noRippleClickable +import com.kindl.core.designsystem.theme.KindlTheme +import com.kindl.presentation.main.MainTab +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun MainBottomBar( + tabs: ImmutableList, + currentTab: MainTab?, + onTabSelected: (MainTab) -> Unit, +) { + Column( + modifier = Modifier + .background(KindlTheme.colors.white), + ) { + HorizontalDivider( + color = KindlTheme.colors.gray100, + thickness = 1.dp, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(), + ) { + tabs.forEach { tab -> + key(tab.route) { + MainBottomBarItem( + tab = tab, + selected = (tab == currentTab), + onClick = { onTabSelected(tab) }, + ) + } + } + } + } +} + +@Composable +private fun RowScope.MainBottomBarItem( + tab: MainTab, + selected: Boolean, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .noRippleClickable(onClick = onClick) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(tab.iconRes), + modifier = Modifier.size(24.dp), + contentDescription = tab.label, + tint = if (selected) { + KindlTheme.colors.hilingualBlack + } else { + KindlTheme.colors.gray200 + }, + ) + Text( + text = tab.label, + color = if (selected) { + KindlTheme.colors.hilingualBlack + } else { + KindlTheme.colors.gray200 + }, + style = KindlTheme.typography.captionR12, + ) + } +} + +@Preview +@Composable +private fun MainBottombarPreview() { + KindlTheme { + MainBottomBar( + tabs = MainTab.entries.toImmutableList(), + currentTab = MainTab.HOME, + onTabSelected = {}, + ) + } +} diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt index 80de192..3ca64f0 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt @@ -20,9 +20,9 @@ internal class MainAppState( val navController: NavHostController, coroutineScope: CoroutineScope, ) { - /*val startDestination = Splash + val startDestination = "Splash" - val isOffline: StateFlow = networkMonitor.isOnline + /*val isOffline: StateFlow = networkMonitor.isOnline .map(Boolean::not) .stateIn( scope = coroutineScope, diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt new file mode 100644 index 0000000..a566468 --- /dev/null +++ b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt @@ -0,0 +1,15 @@ +package com.kindl.presentation.main.state + +import com.kindl.core.permission.PermissionType + +data class MainUiState( + val missingPermissions: List = emptyList(), + val isPermissionCheckComplete: Boolean = false, +) { + val hasPermissions: Boolean get() = missingPermissions.isEmpty() +} + +sealed interface MainSideEffect { + data object CheckPermissions : MainSideEffect + data class OpenSettings(val type: PermissionType) : MainSideEffect +} From a45b5ca96c10c4159af00013c4b6957c3144f055 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 12 May 2026 17:58:21 +0900 Subject: [PATCH 08/17] =?UTF-8?q?[Chore#5]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=EC=8B=A0=EA=B7=9C=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `settings.gradle.kts` 내 모듈 정의 방식을 가독성 향상을 위해 그룹화된 `include` 형식으로 개선하였습니다. - `core` 레이어에 `:core:permission`, `:core:service`, `:core:work` 모듈을 추가하였습니다. - `data` 및 `domain` 레이어를 계층화하고 `:data:forbidden`, `:domain:forbidden` 모듈을 각각 추가하였습니다. - `presentation` 레이어에 `:presentation:home` 모듈을 신규 추가하였습니다. --- .idea/compiler.xml | 2 +- .idea/gradle.xml | 6 ++++++ .idea/misc.xml | 2 +- settings.gradle.kts | 29 ++++++++++++++++++++--------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 5421743..fb7f4a8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -3,4 +3,4 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index f8a571a..6e65b66 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -31,9 +31,15 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 5705276..7146cb1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,4 +7,4 @@ - + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index de68e06..bdf0fbc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,18 +25,29 @@ rootProject.name = "Kindl" include(":app") // core -include(":core:designsystem") -include(":core:common") -include(":core:navigation") -include(":core:network") -include(":core:localstorage") +include( + ":core:designsystem", + ":core:common", + ":core:navigation", + ":core:network", + ":core:localstorage", + ":core:permission", + ":core:service", + ":core:work" +) // data -include(":data") +include( + ":data:forbidden" +) // domain -include(":domain") +include( + ":domain:forbidden" +) // presentation -include(":presentation:main") - +include( + ":presentation:main", + ":presentation:home" +) From 9d3bd773be13e70f9fd5d58484db3024787e136e Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 13 May 2026 17:59:21 +0900 Subject: [PATCH 09/17] =?UTF-8?q?[Feat#5]=20core:localstorage=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `core:localstorage` 모듈을 신규 생성하고 DataStore 의존성 및 기본 빌드 설정을 추가하였습니다. - `DataStoreConstant`를 정의하여 데이터스토어 이름과 알림 거절 횟수 기록을 위한 키(`notification_denied_count`)를 추가하였습니다. - `PermissionInfoManager` 인터페이스와 `PermissionInfoManagerImpl` 구현체를 추가하여 알림 거절 횟수 저장 및 조회 로직을 구현하였습니다. - Hilt를 이용한 의존성 주입을 위해 `LocalStorageModule`을 작성하고 `DataStore` 및 `PermissionInfoManager` 인스턴스 제공 설정을 완료하였습니다. --- core/localstorage/build.gradle.kts | 19 ++++++++++ .../constant/DataStoreConstant.kt | 8 +++++ .../localstorage/di/LocalStorageModule.kt | 35 +++++++++++++++++++ .../permissions/PermissionInfoManager.kt | 9 +++++ .../permissions/PermissionInfoManagerImpl.kt | 28 +++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 core/localstorage/src/main/java/com/focpet/core/localstorage/constant/DataStoreConstant.kt create mode 100644 core/localstorage/src/main/java/com/focpet/core/localstorage/di/LocalStorageModule.kt create mode 100644 core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManager.kt create mode 100644 core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManagerImpl.kt diff --git a/core/localstorage/build.gradle.kts b/core/localstorage/build.gradle.kts index e69de29..6b0ef7c 100644 --- a/core/localstorage/build.gradle.kts +++ b/core/localstorage/build.gradle.kts @@ -0,0 +1,19 @@ +import com.kindl.buildlogic.setNamespace + +plugins { + alias(libs.plugins.kindl.android.library) + alias(libs.plugins.kindl.hilt) + alias(libs.plugins.kindl.serialization) +} + +android { + setNamespace("core.localstorage") +} + +dependencies { + //core + implementation(projects.core.common) + + implementation(libs.javax.inject) + implementation(libs.androidx.datastore.preferences) +} diff --git a/core/localstorage/src/main/java/com/focpet/core/localstorage/constant/DataStoreConstant.kt b/core/localstorage/src/main/java/com/focpet/core/localstorage/constant/DataStoreConstant.kt new file mode 100644 index 0000000..365e1b0 --- /dev/null +++ b/core/localstorage/src/main/java/com/focpet/core/localstorage/constant/DataStoreConstant.kt @@ -0,0 +1,8 @@ +package com.focpet.core.localstorage.constant + +import androidx.datastore.preferences.core.intPreferencesKey + +object DataStoreConstant { + const val DATA_STORE_NAME = "kindl_datastore" + val KEY_NOTIFICATION_DENIED_COUNT = intPreferencesKey("notification_denied_count") +} diff --git a/core/localstorage/src/main/java/com/focpet/core/localstorage/di/LocalStorageModule.kt b/core/localstorage/src/main/java/com/focpet/core/localstorage/di/LocalStorageModule.kt new file mode 100644 index 0000000..579400f --- /dev/null +++ b/core/localstorage/src/main/java/com/focpet/core/localstorage/di/LocalStorageModule.kt @@ -0,0 +1,35 @@ +package com.focpet.core.localstorage.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.focpet.core.localstorage.constant.DataStoreConstant.DATA_STORE_NAME +import com.focpet.core.localstorage.permissions.PermissionInfoManager +import com.focpet.core.localstorage.permissions.PermissionInfoManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocalStorageModule { + private val Context.dataStore: DataStore by preferencesDataStore(name = DATA_STORE_NAME) + + @Provides + @Singleton + fun provideDataStore(@ApplicationContext context: Context): DataStore { + return context.dataStore + } + + @Provides + @Singleton + fun provideOnboardingManager( + dataStore: DataStore, + ): PermissionInfoManager { + return PermissionInfoManagerImpl(dataStore) + } +} diff --git a/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManager.kt b/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManager.kt new file mode 100644 index 0000000..d5272c5 --- /dev/null +++ b/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManager.kt @@ -0,0 +1,9 @@ +package com.focpet.core.localstorage.permissions + +import kotlinx.coroutines.flow.Flow + +interface PermissionInfoManager { + suspend fun saveNotificationDeniedCount() + + val notificationDeniedCount: Flow +} diff --git a/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManagerImpl.kt b/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManagerImpl.kt new file mode 100644 index 0000000..a89c001 --- /dev/null +++ b/core/localstorage/src/main/java/com/focpet/core/localstorage/permissions/PermissionInfoManagerImpl.kt @@ -0,0 +1,28 @@ +package com.focpet.core.localstorage.permissions + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.Preferences +import com.focpet.core.localstorage.constant.DataStoreConstant.KEY_NOTIFICATION_DENIED_COUNT +import com.kindl.core.common.util.suspendRunCatching +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class PermissionInfoManagerImpl @Inject constructor( + private val dataStore: DataStore, +) : PermissionInfoManager { + override suspend fun saveNotificationDeniedCount() { + suspendRunCatching { + dataStore.edit { preferences -> + val currentCount = preferences[KEY_NOTIFICATION_DENIED_COUNT] ?: 0 + preferences[KEY_NOTIFICATION_DENIED_COUNT] = currentCount + 1 + } + } + } + + override val notificationDeniedCount: Flow = dataStore.data + .map { preferences -> + preferences[KEY_NOTIFICATION_DENIED_COUNT] ?: 0 + } +} From 1c53f70cb099569bd92588a2f5787099e2a4efe0 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 13 May 2026 17:59:39 +0900 Subject: [PATCH 10/17] =?UTF-8?q?[Refactor#5]=20`domain:forbidden`=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=82=B4=20=EB=B6=88=EB=B3=80=20=EC=BB=AC?= =?UTF-8?q?=EB=A0=89=EC=85=98=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20UseCase?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `domain:forbidden` 모듈의 `build.gradle.kts`에 `kotlinx.immutable` 라이브러리 의존성을 추가하였습니다. - `GetInstalledAppsUseCase`의 반환 타입을 `List`에서 `ImmutableList`로 변경하여 데이터의 불변성을 보장하고 안정성을 개선하였습니다. --- domain/forbidden/build.gradle.kts | 1 + .../domain/forbidden/usecase/GetInstalledAppsUseCase.kt | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/domain/forbidden/build.gradle.kts b/domain/forbidden/build.gradle.kts index 0856fec..7f24de9 100644 --- a/domain/forbidden/build.gradle.kts +++ b/domain/forbidden/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation(libs.javax.inject) + implementation(libs.kotlinx.immutable) } diff --git a/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt index a57c7fa..92d69e4 100644 --- a/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt +++ b/domain/forbidden/src/main/java/com/kindl/domain/forbidden/usecase/GetInstalledAppsUseCase.kt @@ -2,11 +2,13 @@ package com.kindl.domain.forbidden.usecase import com.kindl.domain.forbidden.model.InstalledAppModel import com.kindl.domain.forbidden.repository.ForbiddenAppRepository +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class GetInstalledAppsUseCase @Inject constructor( private val appRepository: ForbiddenAppRepository, ) { - operator fun invoke(): List = - appRepository.getInstalledUserApps() + operator fun invoke(): ImmutableList = + appRepository.getInstalledUserApps().toImmutableList() } From 4308731908f60eb1fd8794db06bb8980f252c7d3 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 13 May 2026 18:10:02 +0900 Subject: [PATCH 11/17] =?UTF-8?q?[Feat#5]=20=ED=99=88=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=84=A4=EC=B9=98=EB=90=9C?= =?UTF-8?q?=20=EC=95=B1=20=EB=AA=A9=EB=A1=9D=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `presentation:home` 모듈에 `HomeScreen` 및 `HomeRoute`를 추가하고 `LazyColumn`을 사용하여 앱 목록 레이아웃을 구성하였습니다. - `HomeViewModel`과 `HomeState`를 구현하여 `GetInstalledAppsUseCase`로부터 데이터를 가져오고 UI 상태를 관리하는 로직을 추가하였습니다. - `ForbiddenAppUiModel` 및 `ForbiddenAppItem` 컴포넌트를 추가하여 각 앱 항목의 아이콘과 이름을 표시하도록 구현하였습니다. - `HomeNavigation`을 정의하여 `NavGraph` 등록 및 화면 이동을 위한 `navigateToHome` 함수를 추가하였습니다. - `domain:forbidden` 모듈에 대한 의존성을 추가하였습니다. --- presentation/home/build.gradle.kts | 3 +- .../kindl/presentation/home/HomeContract.kt | 11 ++++ .../com/kindl/presentation/home/HomeScreen.kt | 58 +++++++++++++++++++ .../kindl/presentation/home/HomeViewModel.kt | 32 ++++++++++ .../home/component/ForbiddenAppItem.kt | 53 +++++++++++++++++ .../home/model/ForbiddenAppUiModel.kt | 15 +++++ .../home/navigation/HomeNavigation.kt | 29 ++++++++++ 7 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt create mode 100644 presentation/home/src/main/java/com/kindl/presentation/home/navigation/HomeNavigation.kt diff --git a/presentation/home/build.gradle.kts b/presentation/home/build.gradle.kts index 7c39916..a850c91 100644 --- a/presentation/home/build.gradle.kts +++ b/presentation/home/build.gradle.kts @@ -9,5 +9,6 @@ android { } dependencies { - + // domain + implementation(projects.domain.forbidden) } diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt new file mode 100644 index 0000000..e8e5bcd --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt @@ -0,0 +1,11 @@ +package com.kindl.presentation.home + +import androidx.compose.runtime.Immutable +import com.kindl.presentation.home.model.ForbiddenAppUiModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class HomeState( + val forbiddenApps: ImmutableList = persistentListOf(), +) diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt new file mode 100644 index 0000000..a6d164d --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt @@ -0,0 +1,58 @@ +package com.kindl.presentation.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kindl.presentation.home.component.ForbiddenAppItem + +@Composable +internal fun HomeRoute( + paddingValues: PaddingValues, + viewModel: HomeViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + HomeScreen( + paddingValues = paddingValues, + state = state + ) + +} + +@Composable +private fun HomeScreen( + paddingValues: PaddingValues, + state: HomeState +) { + Column ( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(color = Color.White), + horizontalAlignment = Alignment.CenterHorizontally + ) { + LazyColumn ( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + count = state.forbiddenApps.size, + key = { index -> state.forbiddenApps[index].appName } + ) { index -> + ForbiddenAppItem( + item = state.forbiddenApps[index] + ) + } + } + } +} diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt new file mode 100644 index 0000000..1c8487f --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt @@ -0,0 +1,32 @@ +package com.kindl.presentation.home + +import androidx.lifecycle.ViewModel +import com.kindl.domain.forbidden.usecase.GetInstalledAppsUseCase +import com.kindl.presentation.home.model.toUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +internal class HomeViewModel @Inject constructor( + private val getInstalledAppsUseCase: GetInstalledAppsUseCase +) : ViewModel() { + private val _state = MutableStateFlow(HomeState()) + val state = _state.asStateFlow() + + init { + getInstalledApps() + } + + private fun getInstalledApps() { + val installedApps = getInstalledAppsUseCase() + _state.update { currentState -> + currentState.copy( + forbiddenApps = installedApps.map { it.toUiModel() }.toImmutableList() + ) + } + } +} diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt b/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt new file mode 100644 index 0000000..9805914 --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt @@ -0,0 +1,53 @@ +package com.kindl.presentation.home.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kindl.core.designsystem.component.UrlImage +import com.kindl.core.designsystem.theme.KindlTheme +import com.kindl.presentation.home.model.ForbiddenAppUiModel + +@Composable +internal fun ForbiddenAppItem( + item : ForbiddenAppUiModel, + modifier: Modifier = Modifier +) { + Row ( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ){ + UrlImage( + url = item.appIcon, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(16.dp)) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = item.appName + ) + } +} + +@Preview +@Composable +private fun ForbiddenAppItemPreview() { + KindlTheme { + ForbiddenAppItem( + item = ForbiddenAppUiModel( + appIcon = "", + appName = "test" + ) + ) + } +} diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt b/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt new file mode 100644 index 0000000..0b1bfb6 --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt @@ -0,0 +1,15 @@ +package com.kindl.presentation.home.model + +import androidx.compose.runtime.Immutable +import com.kindl.domain.forbidden.model.InstalledAppModel + +@Immutable +data class ForbiddenAppUiModel( + val appIcon: String = "", + val appName: String = "", +) + +internal fun InstalledAppModel.toUiModel() = ForbiddenAppUiModel( + appIcon = packageName, + appName = appName +) diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/navigation/HomeNavigation.kt b/presentation/home/src/main/java/com/kindl/presentation/home/navigation/HomeNavigation.kt new file mode 100644 index 0000000..3e2a21c --- /dev/null +++ b/presentation/home/src/main/java/com/kindl/presentation/home/navigation/HomeNavigation.kt @@ -0,0 +1,29 @@ +package com.kindl.presentation.home.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.kindl.core.navigation.MainTabRoute +import com.kindl.presentation.home.HomeRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateToHome( + navOptions: NavOptions? = null +) { + navigate(Home, navOptions) +} + +fun NavGraphBuilder.homeNavGraph( + paddingValues: PaddingValues +) { + composable { + HomeRoute( + paddingValues = paddingValues + ) + } +} + +@Serializable +data object Home: MainTabRoute From 0039f3d966a2ce2c75554f017726ee4d0d9fae95 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 13 May 2026 18:10:14 +0900 Subject: [PATCH 12/17] =?UTF-8?q?[Feat#5]=20=EA=B6=8C=ED=95=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PermissionScreen`을 추가하여 필수 권한(사용 정보, 다른 앱 위에 표시, 배터리 최적화, 알림) 요청 및 설정 이동 UI를 구현하였습니다. - `MainViewModel`에 `PermissionInfoManager`를 통합하여 알림 권한 거부 횟수 및 영구 거절 상태 관리 로직을 추가하였습니다. - `MainScreen`에서 권한 미부여 시 `PermissionScreen`을 노출하도록 제어하고, 권한 종류 및 상태에 따른 요청 로직(런처 사용 또는 설정 앱 이동)을 세분화하였습니다. - `MainAppState`의 시작 목적지를 `Home`으로 변경하고, `MainTab` 클릭 시 실제 화면 이동이 동작하도록 `NavHost` 설정을 업데이트하였습니다. - `MainBottomBar`와 `MainTab`의 리소스 참조 방식을 개선하고, `presentation:home` 및 `core:localstorage` 의존성을 추가하였습니다. --- presentation/main/build.gradle.kts | 4 + .../com/kindl/presentation/main/MainScreen.kt | 91 +++++++++++++- .../com/kindl/presentation/main/MainTab.kt | 5 +- .../kindl/presentation/main/MainViewModel.kt | 40 ++++++- .../main/component/MainBottomBar.kt | 13 -- .../component/permission/PermissionScreen.kt | 113 ++++++++++++++++++ .../presentation/main/state/MainAppState.kt | 9 +- .../presentation/main/state/MainUiState.kt | 10 +- .../main/res/drawable/baseline_home_24.xml | 5 + 9 files changed, 262 insertions(+), 28 deletions(-) create mode 100644 presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt create mode 100644 presentation/main/src/main/res/drawable/baseline_home_24.xml diff --git a/presentation/main/build.gradle.kts b/presentation/main/build.gradle.kts index 424778a..5a0340c 100644 --- a/presentation/main/build.gradle.kts +++ b/presentation/main/build.gradle.kts @@ -15,4 +15,8 @@ android { dependencies { //core implementation(projects.core.permission) + implementation(projects.core.localstorage) + + //presentation + implementation(projects.presentation.home) } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt index 2277841..b4bd0f0 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt @@ -1,7 +1,17 @@ package com.kindl.presentation.main import MainViewModel +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings import android.widget.Toast +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.tween import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally @@ -17,26 +27,36 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.core.app.ActivityCompat import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost +import com.kindl.core.common.extension.collectSideEffect import com.kindl.core.common.model.DialogTrigger import com.kindl.core.common.model.GlobalUiEventHolder import com.kindl.core.common.model.SnackbarState import com.kindl.core.common.trigger.LocalGlobalUiEventTrigger +import com.kindl.core.permission.PermissionType +import com.kindl.presentation.home.navigation.homeNavGraph import com.kindl.presentation.main.component.MainBottomBar import com.kindl.presentation.main.state.MainAppState +import com.kindl.presentation.main.state.MainSideEffect import com.kindl.presentation.main.state.rememberDialogStateHolder import com.kindl.presentation.main.state.rememberMainAppState +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import androidx.core.net.toUri +import com.kindl.presentation.main.component.permission.PermissionScreen +@SuppressLint("BatteryLife") @Composable internal fun MainScreen( appState: MainAppState = rememberMainAppState(), viewModel: MainViewModel = hiltViewModel(), ) { val context = LocalContext.current + val activity = LocalActivity.current val scope = rememberCoroutineScope() val isBottomBarVisible by appState.isBottomBarVisible.collectAsStateWithLifecycle() @@ -47,6 +67,21 @@ internal fun MainScreen( val snackBarHostState = remember { SnackbarHostState() } var currentSnackbarState by remember { mutableStateOf(null) } + // POST_NOTIFICATIONS 런타임 권한 요청 런처 + val notificationPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + viewModel.checkPermissions() + } else { + val isPermanentlyDenied = !ActivityCompat.shouldShowRequestPermissionRationale( + activity!!, + Manifest.permission.POST_NOTIFICATIONS, + ) + viewModel.onNotificationPermissionDenied(isPermanentlyDenied) + } + } + val onShowToast: (String) -> Unit = remember { { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() @@ -90,6 +125,57 @@ internal fun MainScreen( ) } + viewModel.sideEffect.collectSideEffect { + when (it) { + is MainSideEffect.OpenSettings -> { + context.startActivity(it.intent) + } + + else -> { + + } + } + } + + if (state.isPermissionCheckComplete && !state.hasPermissions) { + PermissionScreen( + missingPermissions = state.missingPermissions, + isPermanentlyDenied = state.isPermanentlyDenied, + notificationDeniedCount = state.notificationDeniedCount, + onRequestPermission = { type -> + when (type) { + PermissionType.POST_NOTIFICATIONS -> { + val neverRequested = state.notificationDeniedCount == 0 + val isPermanentlyDenied = !ActivityCompat.shouldShowRequestPermissionRationale( + activity!!, + Manifest.permission.POST_NOTIFICATIONS, + ) + when { + // 처음 요청 + neverRequested -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + // 영구 거절 → 설정으로 + isPermanentlyDenied -> viewModel.onOpenSettings(type) + // 1회 거절 → 재요청 + else -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + } + // 나머지는 항상 설정으로 + else -> viewModel.onOpenSettings(type) + } + }, + onPermissionGranted = viewModel::checkPermissions, + ) + return + } + CompositionLocalProvider( LocalGlobalUiEventTrigger provides eventHolder, ) { @@ -97,6 +183,7 @@ internal fun MainScreen( bottomBar = { if (isBottomBarVisible == true) { MainBottomBar( + tabs = MainTab.entries.toImmutableList(), currentTab = currentTab, onTabSelected = appState::navigate, ) @@ -133,7 +220,9 @@ internal fun MainScreen( ) }, ) { - + homeNavGraph( + paddingValues = paddingValues + ) } } } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt index 83a54c7..3f8f7d2 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainTab.kt @@ -3,6 +3,7 @@ package com.kindl.presentation.main import androidx.annotation.DrawableRes import com.kindl.core.navigation.MainTabRoute import com.kindl.core.navigation.Route +import com.kindl.presentation.home.navigation.Home internal enum class MainTab( @param:DrawableRes val iconRes: Int, @@ -10,8 +11,8 @@ internal enum class MainTab( val label: String, ) { HOME( - iconRes = com.kindl.core.designsystem.R.drawable.ic_home, - route = MainTabRoute.HOME, + iconRes = R.drawable.baseline_home_24, + route = Home, label = "홈", ); diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt index 1387dbb..4be5a3b 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt @@ -1,17 +1,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.focpet.core.localstorage.permissions.PermissionInfoManager import com.kindl.core.permission.PermissionManager import com.kindl.core.permission.PermissionType import com.kindl.core.permission.usecase.CheckFocusPermissionsUseCase import com.kindl.presentation.main.state.MainSideEffect import com.kindl.presentation.main.state.MainUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,7 +22,8 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val checkFocusPermissions: CheckFocusPermissionsUseCase, - private val permissionManager: PermissionManager, + private val permissionInfoManager: PermissionInfoManager, // 설정 거부 검증용 + private val permissionManager: PermissionManager, // 실제 퍼미션 체크용 ) : ViewModel() { private val _state = MutableStateFlow(MainUiState()) @@ -29,20 +33,46 @@ class MainViewModel @Inject constructor( val sideEffect: SharedFlow = _sideEffect.asSharedFlow() init { - checkPermissions() + viewModelScope.launch { + val deniedCount = permissionInfoManager.notificationDeniedCount.first() + _state.update { + it.copy(notificationDeniedCount = deniedCount) + } + + checkPermissions() + } } fun checkPermissions() { val missingPermissions = checkFocusPermissions() _state.update { it.copy( - missingPermissions = missingPermissions, + missingPermissions = missingPermissions.toImmutableList(), isPermissionCheckComplete = true, ) } } - fun openSettings(type: PermissionType) { - permissionManager.openSettings(type) + fun onNotificationPermissionDenied(isPermanentlyDenied: Boolean) { + viewModelScope.launch { + permissionInfoManager.saveNotificationDeniedCount() + + val deniedCount = permissionInfoManager.notificationDeniedCount.first() + _state.update { + it.copy( + isPermanentlyDenied = isPermanentlyDenied, + notificationDeniedCount = deniedCount, + ) + } + } } + + fun onOpenSettings(type: PermissionType) { + viewModelScope.launch { + val intent = permissionManager.openSettings(type) + _sideEffect.emit(MainSideEffect.OpenSettings(intent)) + } + } + + } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt b/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt index b502206..331950f 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/component/MainBottomBar.kt @@ -33,10 +33,8 @@ internal fun MainBottomBar( ) { Column( modifier = Modifier - .background(KindlTheme.colors.white), ) { HorizontalDivider( - color = KindlTheme.colors.gray100, thickness = 1.dp, ) Row( @@ -76,20 +74,9 @@ private fun RowScope.MainBottomBarItem( imageVector = ImageVector.vectorResource(tab.iconRes), modifier = Modifier.size(24.dp), contentDescription = tab.label, - tint = if (selected) { - KindlTheme.colors.hilingualBlack - } else { - KindlTheme.colors.gray200 - }, ) Text( text = tab.label, - color = if (selected) { - KindlTheme.colors.hilingualBlack - } else { - KindlTheme.colors.gray200 - }, - style = KindlTheme.typography.captionR12, ) } } diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt b/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt new file mode 100644 index 0000000..15649ef --- /dev/null +++ b/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt @@ -0,0 +1,113 @@ +package com.kindl.presentation.main.component.permission + +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.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.kindl.core.designsystem.theme.KindlTheme +import com.kindl.core.permission.PermissionType +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun PermissionScreen( + missingPermissions: ImmutableList, + isPermanentlyDenied: Boolean, + notificationDeniedCount: Int, + onRequestPermission: (PermissionType) -> Unit, + onPermissionGranted: () -> Unit, +) { + LifecycleResumeEffect(Unit) { + onPermissionGranted() + + onPauseOrDispose { + + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + missingPermissions.forEach { type -> + PermissionItem( + type = type, + isPermanentlyDenied = isPermanentlyDenied && type == PermissionType.POST_NOTIFICATIONS, + notificationDeniedCount = notificationDeniedCount, + onRequest = { onRequestPermission(type) }, + ) + } + } +} + +@Composable +private fun PermissionItem( + type: PermissionType, + isPermanentlyDenied: Boolean, + notificationDeniedCount: Int, + onRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + val (title, description) = when (type) { + PermissionType.USAGE_STATS -> "앱 사용 정보" to "금지 앱 감지를 위해 필요해요" + PermissionType.OVERLAY -> "다른 앱 위에 표시" to "집중 방해 시 경고창 표시에 필요해요" + PermissionType.BATTERY_OPTIMIZATION -> "배터리 최적화 제외" to "백그라운드 감지를 위해 필요해요" + PermissionType.POST_NOTIFICATIONS -> "알림 권한" to "집중 시간 알림을 위해 필요해요" + } + + val buttonText = when { + type == PermissionType.POST_NOTIFICATIONS && isPermanentlyDenied -> "설정에서 허용" + else -> "권한 허용" + } + + val descriptionText = when { + type == PermissionType.POST_NOTIFICATIONS && isPermanentlyDenied -> + "설정에서 직접 알림 권한을 허용해주세요" + else -> description + } + + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = title) + Text(text = descriptionText) + } + Button(onClick = onRequest) { + Text(text = buttonText) + } + } +} + +@Preview +@Composable +private fun PermissionItemPreview() { + KindlTheme { + PermissionItem( + type = PermissionType.POST_NOTIFICATIONS, + isPermanentlyDenied = false, + notificationDeniedCount = 0, + onRequest = {}, + ) + } +} diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt index 3ca64f0..e26dcc7 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainAppState.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import com.kindl.presentation.home.navigation.Home import com.kindl.presentation.main.MainTab import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -20,7 +21,7 @@ internal class MainAppState( val navController: NavHostController, coroutineScope: CoroutineScope, ) { - val startDestination = "Splash" + val startDestination = Home /*val isOffline: StateFlow = networkMonitor.isOnline .map(Boolean::not) @@ -81,9 +82,9 @@ internal class MainAppState( launchSingleTop } - /*when (tab) { - - }*/ + when (tab) { + MainTab.HOME -> navController.navigate(Home, navOptions) + } } private val clearStackNavOptions = navOptions { diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt index a566468..7acd5d3 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt @@ -1,15 +1,19 @@ package com.kindl.presentation.main.state +import android.content.Intent import com.kindl.core.permission.PermissionType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf data class MainUiState( - val missingPermissions: List = emptyList(), + val missingPermissions: ImmutableList = persistentListOf(), val isPermissionCheckComplete: Boolean = false, + val isPermanentlyDenied: Boolean = false, + val notificationDeniedCount: Int = 0, ) { val hasPermissions: Boolean get() = missingPermissions.isEmpty() } sealed interface MainSideEffect { - data object CheckPermissions : MainSideEffect - data class OpenSettings(val type: PermissionType) : MainSideEffect + data class OpenSettings(val intent: Intent) : MainSideEffect } diff --git a/presentation/main/src/main/res/drawable/baseline_home_24.xml b/presentation/main/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000..20cb4d6 --- /dev/null +++ b/presentation/main/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,5 @@ + + + + + From 492dddc51b42a6c7d6a37805ddcd297253d8f828 Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 14 May 2026 12:59:00 +0900 Subject: [PATCH 13/17] =?UTF-8?q?[Refactor#5]=20`UrlImage`=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9C=A0=EC=97=B0=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=AA=A8=EB=93=88=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `UrlImage` 컴포넌트의 `url` 매개변수 타입을 `String`에서 `Any?`로 변경하여 다양한 이미지 소스 타입을 지원하도록 개선하였습니다. - `app` 모듈의 `build.gradle.kts`에 `data:forbidden` 모듈 의존성을 추가하였습니다. --- .idea/misc.xml | 1 - app/build.gradle.kts | 3 +++ .../java/com/kindl/core/designsystem/component/UrlImage.kt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 7146cb1..d852bbf 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab8b170..e8e9d4a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,9 @@ dependencies { // core implementation(projects.core.network) + // data + implementation(projects.data.forbidden) + // presentation implementation(projects.presentation.main) diff --git a/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt b/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt index 3fbfba6..df30678 100644 --- a/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt +++ b/core/designsystem/src/main/java/com/kindl/core/designsystem/component/UrlImage.kt @@ -15,7 +15,7 @@ import com.kindl.core.designsystem.R @Composable fun UrlImage( - url: String, + url: Any?, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit, contentDescription: String? = null, From 24ee62f27c4d9f3651768cfff2dae624f20a9a8b Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 14 May 2026 12:59:45 +0900 Subject: [PATCH 14/17] =?UTF-8?q?[Chore#5]=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PermissionManagerImpl`에서 불필요한 `@Singleton` 어노테이션을 제거하고 미사용 임포트를 정리하였습니다. - `ForbiddenRepositoryModule`의 코드 가독성 향상을 위해 공백을 추가하였습니다. - `data` 및 `domain` 디렉토리 내의 불필요한 `.gitkeep` 파일을 삭제하였습니다. --- .../com/kindl/core/permission/PermissionManagerImpl.kt | 5 +---- data/.gitkeep | 0 .../com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt | 1 + domain/.gitkeep | 0 4 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 data/.gitkeep delete mode 100644 domain/.gitkeep diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt index 64a3b73..be94a16 100644 --- a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt @@ -2,14 +2,11 @@ package com.kindl.core.permission import android.content.Context import android.content.Intent -import android.net.Uri import android.provider.Settings +import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -import javax.inject.Singleton -import androidx.core.net.toUri -@Singleton internal class PermissionManagerImpl @Inject constructor( @param:ApplicationContext private val context: Context, ) : PermissionManager { diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt index 2d8986e..b7953a6 100644 --- a/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt +++ b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt @@ -11,6 +11,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface ForbiddenRepositoryModule { + @Binds @Singleton fun bindForbiddenRepository( diff --git a/domain/.gitkeep b/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 From 0baf3f20edd36a37ef8ab1ce2ee56f54140ab777 Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 14 May 2026 13:00:11 +0900 Subject: [PATCH 15/17] =?UTF-8?q?[Refactor#5]=20`ForbiddenAppUiModel`=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=95=B1?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=A1=9C=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ForbiddenAppUiModel`에서 `appIcon` 필드를 `icon: Drawable?`로 변경하고, 고유 식별을 위한 `appPackageName` 필드를 추가하였습니다. - `HomeViewModel`에서 `PackageManager`를 사용하여 설치된 앱의 실제 아이콘을 로드하는 비동기 로직(`loadApps`)을 구현하였습니다. - `HomeScreen`의 `LazyColumn`에서 `appPackageName`을 키(key)로 사용하고 `contentType`을 정의하여 리스트 렌더링 성능을 최적화하였습니다. - `ForbiddenAppUiModel`의 어노테이션을 `@Immutable`에서 `@Stable`로 변경하였습니다. - `presentation:main` 모듈의 패키지 경로 누락 수정 및 불필요한 import 구문을 정리하였습니다. --- .../kindl/presentation/home/HomeContract.kt | 2 +- .../com/kindl/presentation/home/HomeScreen.kt | 9 +++--- .../kindl/presentation/home/HomeViewModel.kt | 30 +++++++++++++------ .../home/component/ForbiddenAppItem.kt | 14 ++++----- .../home/model/ForbiddenAppUiModel.kt | 14 ++++----- .../com/kindl/presentation/main/MainScreen.kt | 8 +---- .../kindl/presentation/main/MainViewModel.kt | 2 ++ 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt index e8e5bcd..7d968ed 100644 --- a/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeContract.kt @@ -8,4 +8,4 @@ import kotlinx.collections.immutable.persistentListOf @Immutable data class HomeState( val forbiddenApps: ImmutableList = persistentListOf(), -) +) \ No newline at end of file diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt index a6d164d..25037f3 100644 --- a/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeScreen.kt @@ -41,17 +41,16 @@ private fun HomeScreen( .background(color = Color.White), horizontalAlignment = Alignment.CenterHorizontally ) { - LazyColumn ( + LazyColumn( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally ) { items( count = state.forbiddenApps.size, - key = { index -> state.forbiddenApps[index].appName } + key = { index -> state.forbiddenApps[index].appPackageName }, + contentType = { "forbidden_app" } ) { index -> - ForbiddenAppItem( - item = state.forbiddenApps[index] - ) + ForbiddenAppItem(item = state.forbiddenApps[index]) } } } diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt b/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt index 1c8487f..2c0cc20 100644 --- a/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt +++ b/presentation/home/src/main/java/com/kindl/presentation/home/HomeViewModel.kt @@ -1,32 +1,44 @@ package com.kindl.presentation.home +import android.content.Context import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.kindl.domain.forbidden.usecase.GetInstalledAppsUseCase -import com.kindl.presentation.home.model.toUiModel +import com.kindl.presentation.home.model.ForbiddenAppUiModel import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel internal class HomeViewModel @Inject constructor( - private val getInstalledAppsUseCase: GetInstalledAppsUseCase + @param:ApplicationContext private val context: Context, + private val getInstalledAppsUseCase: GetInstalledAppsUseCase, ) : ViewModel() { private val _state = MutableStateFlow(HomeState()) val state = _state.asStateFlow() init { - getInstalledApps() + loadApps() } - private fun getInstalledApps() { - val installedApps = getInstalledAppsUseCase() - _state.update { currentState -> - currentState.copy( - forbiddenApps = installedApps.map { it.toUiModel() }.toImmutableList() - ) + private fun loadApps() { + viewModelScope.launch(Dispatchers.IO) { + val pm = context.packageManager + val uiModels = getInstalledAppsUseCase().map { app -> + ForbiddenAppUiModel( + appPackageName = app.packageName, + appName = app.appName, + icon = runCatching { pm.getApplicationIcon(app.packageName) }.getOrNull(), + ) + }.toImmutableList() + + _state.update { it.copy(forbiddenApps = uiModels) } } } } diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt b/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt index 9805914..1298f7f 100644 --- a/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt +++ b/presentation/home/src/main/java/com/kindl/presentation/home/component/ForbiddenAppItem.kt @@ -17,15 +17,15 @@ import com.kindl.presentation.home.model.ForbiddenAppUiModel @Composable internal fun ForbiddenAppItem( - item : ForbiddenAppUiModel, + item: ForbiddenAppUiModel, modifier: Modifier = Modifier ) { - Row ( + Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically - ){ + ) { UrlImage( - url = item.appIcon, + url = item.icon, modifier = Modifier .size(36.dp) .clip(RoundedCornerShape(16.dp)) @@ -33,9 +33,7 @@ internal fun ForbiddenAppItem( Spacer(modifier = Modifier.weight(1f)) - Text( - text = item.appName - ) + Text(text = item.appName) } } @@ -45,7 +43,7 @@ private fun ForbiddenAppItemPreview() { KindlTheme { ForbiddenAppItem( item = ForbiddenAppUiModel( - appIcon = "", + appPackageName = "", appName = "test" ) ) diff --git a/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt b/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt index 0b1bfb6..106cfd5 100644 --- a/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt +++ b/presentation/home/src/main/java/com/kindl/presentation/home/model/ForbiddenAppUiModel.kt @@ -1,15 +1,11 @@ package com.kindl.presentation.home.model -import androidx.compose.runtime.Immutable -import com.kindl.domain.forbidden.model.InstalledAppModel +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Stable -@Immutable +@Stable data class ForbiddenAppUiModel( - val appIcon: String = "", + val appPackageName: String = "", val appName: String = "", -) - -internal fun InstalledAppModel.toUiModel() = ForbiddenAppUiModel( - appIcon = packageName, - appName = appName + val icon: Drawable? = null, ) diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt index b4bd0f0..cf432f0 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainScreen.kt @@ -1,13 +1,8 @@ package com.kindl.presentation.main -import MainViewModel import android.Manifest import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.net.Uri import android.os.Build -import android.provider.Settings import android.widget.Toast import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult @@ -39,6 +34,7 @@ import com.kindl.core.common.trigger.LocalGlobalUiEventTrigger import com.kindl.core.permission.PermissionType import com.kindl.presentation.home.navigation.homeNavGraph import com.kindl.presentation.main.component.MainBottomBar +import com.kindl.presentation.main.component.permission.PermissionScreen import com.kindl.presentation.main.state.MainAppState import com.kindl.presentation.main.state.MainSideEffect import com.kindl.presentation.main.state.rememberDialogStateHolder @@ -46,8 +42,6 @@ import com.kindl.presentation.main.state.rememberMainAppState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import androidx.core.net.toUri -import com.kindl.presentation.main.component.permission.PermissionScreen @SuppressLint("BatteryLife") @Composable diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt index 4be5a3b..92fddf9 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/MainViewModel.kt @@ -1,3 +1,5 @@ +package com.kindl.presentation.main + import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.focpet.core.localstorage.permissions.PermissionInfoManager From 1af4a9ae95f308754ba051cebc8fed5d993e6bec Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 19 May 2026 10:56:42 +0900 Subject: [PATCH 16/17] =?UTF-8?q?[Feat#5]=20=EC=A0=91=EA=B7=BC=EC=84=B1=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PermissionType`에 `ACCESSIBILITY`를 추가하고, `POST_NOTIFICATIONS`의 필수 여부(`isFocusRequired`)를 `false`로 변경하였습니다. - `PermissionChecker`에 `Accessibility` 객체를 추가하여 앱의 접근성 서비스 활성화 여부를 확인하는 로직을 구현하였습니다. - `PermissionManager` 인터페이스의 `openSettings` 반환 타입을 `Intent?`로 변경하고, `PermissionManagerImpl`에서 접근성 설정 화면으로 이동하는 인텐트 처리 로직을 추가하였습니다. - `PermissionChecker.allGranted` 컴패니언 함수를 추가하여 모든 권한의 부여 상태를 한 번에 확인할 수 있도록 개선하였습니다. - `PermissionChecker` 내 불필요한 어노테이션(`@SuppressLint`, `@Suppress`)을 제거하고 코드를 정리하였습니다. --- .../core/permission/PermissionChecker.kt | 27 ++++++++++++++++--- .../core/permission/PermissionManager.kt | 2 +- .../core/permission/PermissionManagerImpl.kt | 3 +++ .../kindl/core/permission/PermissionType.kt | 9 ++++--- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt index ad32976..21ff798 100644 --- a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionChecker.kt @@ -1,7 +1,6 @@ package com.kindl.core.permission import android.Manifest -import android.annotation.SuppressLint import android.app.AppOpsManager import android.content.Context import android.content.pm.PackageManager @@ -17,7 +16,6 @@ internal fun interface PermissionChecker { override fun isGranted(context: Context): Boolean { val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - @Suppress("DEPRECATION") appOps.unsafeCheckOpNoThrow( AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), @@ -40,7 +38,6 @@ internal fun interface PermissionChecker { } object BatteryOptimization : PermissionChecker { - @SuppressLint("ServiceCast") override fun isGranted(context: Context): Boolean { val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager return pm.isIgnoringBatteryOptimizations(context.packageName) @@ -51,9 +48,31 @@ internal fun interface PermissionChecker { override fun isGranted(context: Context): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED + PackageManager.PERMISSION_GRANTED } else { true } } + + object Accessibility : PermissionChecker { + override fun isGranted(context: Context): Boolean { + val enabledServices = Settings.Secure.getString( + context.contentResolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + ) ?: return false + return enabledServices.split(":") + .any { it.startsWith(context.packageName) } + } + } + + companion object { + fun allGranted(context: Context): Boolean = + listOf( + UsageStats, + Overlay, + BatteryOptimization, + PostNotifications, + Accessibility, + ).all { it.isGranted(context) } + } } diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt index edaed58..8cca078 100644 --- a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManager.kt @@ -6,5 +6,5 @@ interface PermissionManager { fun check(type: PermissionType): Boolean fun allFocusPermissionsGranted(): Boolean fun missingFocusPermissions(): List - fun openSettings(type: PermissionType) : Intent + fun openSettings(type: PermissionType) : Intent? } diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt index be94a16..6739e73 100644 --- a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionManagerImpl.kt @@ -47,6 +47,9 @@ internal class PermissionManagerImpl @Inject constructor( Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) } + + PermissionType.ACCESSIBILITY -> + Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) }.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } diff --git a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt index 4a10421..ac69d3d 100644 --- a/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt +++ b/core/permission/src/main/kotlin/com/kindl/core/permission/PermissionType.kt @@ -1,8 +1,9 @@ package com.kindl.core.permission enum class PermissionType(val isFocusRequired: Boolean) { - USAGE_STATS(true), - OVERLAY(true), - BATTERY_OPTIMIZATION(true), - POST_NOTIFICATIONS(true), + USAGE_STATS(isFocusRequired = true), + OVERLAY(isFocusRequired = true), + BATTERY_OPTIMIZATION(isFocusRequired = true), + POST_NOTIFICATIONS(isFocusRequired = false), + ACCESSIBILITY(isFocusRequired = true), } From 9963ba04cee3e21ba816c10fa68c4f59a0ed6db3 Mon Sep 17 00:00:00 2001 From: sonms Date: Tue, 19 May 2026 11:08:24 +0900 Subject: [PATCH 17/17] =?UTF-8?q?[Feat#5]=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PermissionScreen`에 `ACCESSIBILITY` 권한 타입을 추가하고, 권한 요청을 위한 UI 문구(접근 권한)를 정의하였습니다. - `ForbiddenAppRepositoryImpl` 및 `ForbiddenRepositoryModule`에 `internal` 접근 제어자를 적용하여 모듈 내 캡슐화를 강화하였습니다. - `MainSideEffect.OpenSettings`의 `intent` 매개변수 타입을 `Intent?`로 변경하여 Null 허용이 가능하도록 수정하였습니다. --- .../com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt | 2 +- .../data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt | 2 +- .../presentation/main/component/permission/PermissionScreen.kt | 1 + .../main/java/com/kindl/presentation/main/state/MainUiState.kt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt index b7953a6..cca6105 100644 --- a/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt +++ b/data/forbidden/src/main/java/com/kindl/data/forbidden/di/ForbiddenRepositoryModule.kt @@ -10,7 +10,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface ForbiddenRepositoryModule { +internal interface ForbiddenRepositoryModule { @Binds @Singleton diff --git a/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt b/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt index 5af0d7f..a329a9d 100644 --- a/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt +++ b/data/forbidden/src/main/java/com/kindl/data/forbidden/repositoryimpl/ForbiddenAppRepositoryImpl.kt @@ -7,7 +7,7 @@ import com.kindl.domain.forbidden.repository.ForbiddenAppRepository import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class ForbiddenAppRepositoryImpl @Inject constructor( +internal class ForbiddenAppRepositoryImpl @Inject constructor( @param:ApplicationContext private val context: Context, ) : ForbiddenAppRepository { override fun getInstalledUserApps(): List { diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt b/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt index 15649ef..311c7d3 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/component/permission/PermissionScreen.kt @@ -69,6 +69,7 @@ private fun PermissionItem( PermissionType.OVERLAY -> "다른 앱 위에 표시" to "집중 방해 시 경고창 표시에 필요해요" PermissionType.BATTERY_OPTIMIZATION -> "배터리 최적화 제외" to "백그라운드 감지를 위해 필요해요" PermissionType.POST_NOTIFICATIONS -> "알림 권한" to "집중 시간 알림을 위해 필요해요" + PermissionType.ACCESSIBILITY -> "접근 권한" to "앱 접근 시 필요해요" } val buttonText = when { diff --git a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt index 7acd5d3..3a0a12d 100644 --- a/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt +++ b/presentation/main/src/main/java/com/kindl/presentation/main/state/MainUiState.kt @@ -15,5 +15,5 @@ data class MainUiState( } sealed interface MainSideEffect { - data class OpenSettings(val intent: Intent) : MainSideEffect + data class OpenSettings(val intent: Intent?) : MainSideEffect }