Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aa12de4
[setting#5] 앱 구동 및 포그라운드 서비스를 위한 Android 권한 설정 추가
sonms May 12, 2026
ada581c
[Feat#5] UrlImage 공통 컴포넌트 추가 및 Coil 라이브러리 의존성 추가
sonms May 12, 2026
5d49856
[feat#5] core:permission 모듈 추가 및 권한 관리 로직 구현
sonms May 12, 2026
b0efbb2
[Add#5] core:service 및 core:work 모듈 추가
sonms May 12, 2026
ce404fd
[Feat#5] data:forbidden 모듈 추가 및 설치된 앱 목록 조회 기능 구현
sonms May 12, 2026
da547f0
[Feat#5] `domain:forbidden` 모듈 생성 및 설치된 앱 조회 기능 구현
sonms May 12, 2026
e7ab420
[Feat#5] presentation:home 모듈 추가 및 메인 화면 네비게이션/권한 로직 구현
sonms May 12, 2026
a45b5ca
[Chore#5] 프로젝트 모듈 구조 확장 및 신규 모듈 추가
sonms May 12, 2026
9d3bd77
[Feat#5] core:localstorage 모듈 추가 및 권한 정보 관리 기능 구현
sonms May 13, 2026
1c53f70
[Refactor#5] `domain:forbidden` 모듈 내 불변 컬렉션 적용 및 UseCase 반환 타입 변경
sonms May 13, 2026
4308731
[Feat#5] 홈 화면 구현 및 설치된 앱 목록 표시 기능 추가
sonms May 13, 2026
0039f3d
[Feat#5] 권한 관리 화면 구현 및 메인 네비게이션 연동
sonms May 13, 2026
492dddc
[Refactor#5] `UrlImage` 컴포넌트 유연성 개선 및 모듈 의존성 추가
sonms May 14, 2026
24ee62f
[Chore#5] 코드 스타일 개선 및 불필요한 프로젝트 파일 정리
sonms May 14, 2026
0baf3f2
[Refactor#5] `ForbiddenAppUiModel` 구조 개선 및 앱 아이콘 로드 로직 수정
sonms May 14, 2026
1af4a9a
[Feat#5] 접근성 권한 추가 및 권한 관리 로직 개선
sonms May 19, 2026
9963ba0
[Feat#5] 접근 권한 추가 및 데이터 레이어 접근 제어자 수정
sonms May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ dependencies {
// core
implementation(projects.core.network)

// data
implementation(projects.data.forbidden)

// presentation
implementation(projects.presentation.main)

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />

<application
android:name="com.kindl.KindlApplication"
android:allowBackup="true"
Expand Down
4 changes: 4 additions & 0 deletions core/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ plugins {
android {
setNamespace("core.designsystem")
}

dependencies {
implementation(libs.coil.compose)
}
Original file line number Diff line number Diff line change
@@ -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: Any?,
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),
)
}
10 changes: 10 additions & 0 deletions core/designsystem/src/main/res/drawable/img_fake_red.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M0,0h100v100h-100z"
android:fillColor="#FF3737"/>
</vector>
19 changes: 19 additions & 0 deletions core/localstorage/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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<Preferences> by preferencesDataStore(name = DATA_STORE_NAME)

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return context.dataStore
}

@Provides
@Singleton
fun provideOnboardingManager(
dataStore: DataStore<Preferences>,
): PermissionInfoManager {
return PermissionInfoManagerImpl(dataStore)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.focpet.core.localstorage.permissions

import kotlinx.coroutines.flow.Flow

interface PermissionInfoManager {
suspend fun saveNotificationDeniedCount()

val notificationDeniedCount: Flow<Int>
}
Original file line number Diff line number Diff line change
@@ -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<Preferences>,
) : 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<Int> = dataStore.data
.map { preferences ->
preferences[KEY_NOTIFICATION_DENIED_COUNT] ?: 0
}
}
10 changes: 10 additions & 0 deletions core/permission/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 2 additions & 0 deletions core/permission/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.kindl.core.permission

import android.Manifest
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) {
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 {
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
}
}

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) }
}
}
Original file line number Diff line number Diff line change
@@ -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<PermissionType>
fun openSettings(type: PermissionType) : Intent?
}
Loading
Loading