This commit is contained in:
lunaticbum 2026-03-04 18:00:00 +09:00
parent 614e244be7
commit 83546e5e10
4 changed files with 184 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package bums.lunatic.launcher.apps
import android.app.Dialog import android.app.Dialog
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -28,6 +29,7 @@ import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.CategoryGrouper import bums.lunatic.launcher.utils.CategoryGrouper
import bums.lunatic.launcher.utils.CategoryManualMapper
import bums.lunatic.launcher.utils.EnToKo import bums.lunatic.launcher.utils.EnToKo
import bums.lunatic.launcher.utils.JamoUtils import bums.lunatic.launcher.utils.JamoUtils
import bums.lunatic.launcher.utils.SimpleTransliterater import bums.lunatic.launcher.utils.SimpleTransliterater
@ -97,18 +99,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
return binding.root return binding.root
} }
private val categoryMap = mapOf(
"전체" to "ALL",
"게임" to "GAME",
"생산성" to "PRODUCTIVITY",
"소셜" to "SOCIAL",
"오디오" to "AUDIO",
"비디오" to "VIDEO",
"이미지" to "IMAGE",
"지도" to "MAPS",
"뉴스" to "NEWS",
"기타" to "UNDEFINED" // 혹은 UNKNOWN
)
private var currentScope: String = CategoryGrouper.SCOPE_ALL private var currentScope: String = CategoryGrouper.SCOPE_ALL
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -127,7 +118,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
private var currentCategoryKey: String = "ALL" private var currentCategoryKey: String = "ALL"
private fun setupCategorySpinner() { private fun setupCategorySpinner() {
// 1. 스피너에 들어갈 데이터(표시 이름들) 준비 // 1. 스피너에 들어갈 데이터(표시 이름들) 준비
val displayList = categoryMap.keys.toList() val displayList = CategoryManualMapper.CATEGORY_MAP.keys.toList()
// 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용) // 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용)
val adapter = object : ArrayAdapter<String>( val adapter = object : ArrayAdapter<String>(
@ -154,7 +145,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedDisplayName = displayList[position] val selectedDisplayName = displayList[position]
currentCategoryKey = categoryMap[selectedDisplayName] ?: "ALL" currentCategoryKey = CategoryManualMapper.CATEGORY_MAP[selectedDisplayName] ?: "ALL"
// 선택 변경 시 목록 갱신 (검색어 유지) // 선택 변경 시 목록 갱신 (검색어 유지)
fetchApps(binding.searchInput.text.toString()) fetchApps(binding.searchInput.text.toString())
@ -421,6 +412,8 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
} }
/** /**
* 목록을 불러오는 함수 * 목록을 불러오는 함수
* - 검색어(keyword) 있으면 필터링을 수행합니다. * - 검색어(keyword) 있으면 필터링을 수행합니다.
@ -467,7 +460,15 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
.sort("lastUseDate", Sort.DESCENDING) .sort("lastUseDate", Sort.DESCENDING)
.find() .find()
val scopedAppsList = baseAppQuery.map { realm.copyFromRealm(it) } val scopedAppsList = baseAppQuery.map {
val appCopy = realm.copyFromRealm(it)
// 💡 수동 매퍼를 사용하여 카테고리 강제 재할당
// it.systemCategoryInt는 AppInfo 모델에 시스템 카테고리 값이 저장되어 있다고 가정함
val fixedCategory = CategoryManualMapper.getFixedCategory(it.pkgName ?:"", it.appName, it.systemCategoryInt)
appCopy.category = fixedCategory
appCopy
}
.filter { appInfo -> .filter { appInfo ->
val isLaunchable = try { val isLaunchable = try {
pm.getLaunchIntentForPackage(appInfo.pkgName ?: "") != null pm.getLaunchIntentForPackage(appInfo.pkgName ?: "") != null
@ -479,7 +480,6 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val isCategoryMatch = if (currentCategoryKey == "ALL") { val isCategoryMatch = if (currentCategoryKey == "ALL") {
true true
} else { } else {
// AppInfoGetter에서 저장한 값("GAME", "AUDIO" 등)과 비교
appInfo.category == currentCategoryKey appInfo.category == currentCategoryKey
} }

View File

@ -16,6 +16,7 @@ class AppInfo : RealmObject {
var clickCount : Int = 0 var clickCount : Int = 0
var lastUseDate : Long = 0L var lastUseDate : Long = 0L
var category : String? = null var category : String? = null
var systemCategoryInt : Int = 0
var currentInstalled : Boolean = false var currentInstalled : Boolean = false
var isInstalled : Boolean = false var isInstalled : Boolean = false
// [신규] 0: 항상 보이기(기본), 1: 검색 시만 보이기 (숨김) // [신규] 0: 항상 보이기(기본), 1: 검색 시만 보이기 (숨김)

View File

@ -0,0 +1,166 @@
package bums.lunatic.launcher.utils
import android.content.pm.ApplicationInfo
import java.util.Locale
/**
* 로그 분석 키워드 기반 카테고리 자동 분류 유틸리티
*/
object CategoryManualMapper {
val CATEGORY_MAP = mapOf(
"전체" to "ALL",
"게임" to "GAME",
"생산성" to "PRODUCTIVITY",
"소셜" to "SOCIAL",
"오디오" to "MUSIC",
"쇼핑" to "SHOPPING",
"금융" to "FINANCE",
"비디오" to "VIDEO",
"이미지" to "IMAGE",
"지도" to "MAPS",
"뉴스" to "NEWS",
"도구" to "TOOLS",
"기타" to "UNDEFINED"
)
// 1. 고정 패키지명 매핑 (가장 높은 우선순위)
private val manualMapping = mapOf(
"com.sec.android.app.myfiles" to "TOOLS", // 삼성 내 파일
"com.google.android.apps.nbu.files" to "TOOLS", // 구글 Files
// VIDEO 카테고리
"com.netflix.mediaclient" to "VIDEO",
"net.cj.cjhv.gs.tving" to "VIDEO",
"com.google.android.youtube" to "VIDEO",
"com.disney.disneyplus" to "VIDEO",
"com.coupang.mobile.play" to "VIDEO",
"kr.co.captv.pooqV2" to "VIDEO", // wavve
"com.synology.dsvideo" to "VIDEO",
"org.videolan.vlc" to "VIDEO",
"com.mxtech.videoplayer.ad" to "VIDEO",
"com.gretech.gomplayerko" to "VIDEO",
"com.google.android.videos" to "VIDEO", // Google TV
// MAPS / TRAVEL 카테고리
"com.nhn.android.nmap" to "MAPS",
"net.daum.android.map" to "MAPS",
"com.google.android.apps.maps" to "MAPS",
"com.skt.tmap.ku" to "MAPS",
"com.locnall.KimGiSa" to "MAPS", // 카카오내비
"com.agoda.mobile.consumer" to "MAPS",
"com.airbnb.android" to "MAPS",
"com.mrt.ducati" to "MAPS", // 마이리얼트립
// FINANCE 카테고리
"viva.republica.toss" to "FINANCE",
"com.kakaobank.channel" to "FINANCE",
"com.btckorea.bithumb" to "FINANCE",
"com.dunamu.exchange" to "FINANCE", // 업비트
"com.truefriend.ministock" to "FINANCE",
"com.truefriend.neosmartarenewal" to "FINANCE",
"com.hyundaicard.appcard" to "FINANCE",
"kr.co.samsungcard.mpocket" to "FINANCE",
"com.kbcard.cxh.appcard" to "FINANCE",
"com.kbstar.kbbank" to "FINANCE",
"nh.smart.banking" to "FINANCE",
"com.kbankwith.smartbank" to "FINANCE",
// SHOPPING 카테고리
"com.coupang.mobile" to "SHOPPING",
"com.coupang.mobile.eats" to "SHOPPING",
"com.ebay.kr.gmarket" to "SHOPPING",
"com.ebay.kr.auction" to "SHOPPING",
"com.elevenst" to "SHOPPING",
"net.giosis.shopping.sg" to "SHOPPING", // Qoo10
"com.alibaba.aliexpresshd" to "SHOPPING",
"com.einnovation.temu" to "SHOPPING",
// GAME 카테고리
"com.sundaytoz.mobile.anisachun.google.service" to "GAME",
"com.gof.global" to "GAME", // 화이트아웃서바이벌
"com.tap4fun.odin.kingdomguard" to "GAME",
"com.dreamgames.royalmatch" to "GAME",
"com.ragequitgames.tomorrow" to "GAME"
)
// 2. 키워드 기반 카테고리 매핑 (두 번째 우선순위)
private val keywordMapping = mapOf(
"VIDEO" to listOf(
"video", "player", "movie", "tv", "cinema", "streaming", "media", "netflix",
"비디오", "플레이어", "영화", "티브이", "시네마", "스트리밍", "넷플릭스", "티빙", "유튜브"
),
"GAME" to listOf(
"game", "vulkan", "unity", "unreal", "nexon", "netmarble", "lineage",
"게임", "넥슨", "넷마블", "카카오게임", "애니팡", "전투", "전략", "퍼즐", "RPG"
),
"FINANCE" to listOf(
"bank", "card", "finance", "pay", "stock", "invest", "kb", "shinhan", "woori", "hana",
"은행", "카드", "금융", "페이", "증권", "주식", "투자", "뱅크", "뱅킹", "보험", "자산"
),
"MUSIC" to listOf(
"music", "audio", "sound", "radio", "melody", "streaming", "melon", "bugs",
"뮤직", "음악", "오디오", "사운드", "라디오", "멜로디", "멜론", "벅스", "지니"
),
"MAPS" to listOf(
"map", "navi", "taxi", "transport", "bus", "subway", "metro", "navigation",
"지도", "내비", "택시", "교통", "버스", "지하철", "네비", "길찾기"
),
"IMAGE" to listOf(
"photo", "gallery", "image", "camera", "editor", "snap", "pic",
"사진", "갤러리", "이미지", "카메라", "편집", "스냅", "뷰어"
),
"SOCIAL" to listOf(
"talk", "messenger", "chat", "social", "sns", "community", "kakao", "telegram",
"", "메신저", "채팅", "소셜", "커뮤니티", "카카오", "텔레그램", "밴드", "카페"
),
"SHOPPING" to listOf(
"shop", "mall", "market", "delivery", "eats", "order", "commerce", "coupang",
"쇼핑", "", "마켓", "배달", "이츠", "주문", "커머스", "쿠팡", "마트", "백화점"
),
"TOOLS" to listOf(
"file", "manager", "explorer", "cleaner", "storage", "calculator", "clock", "calendar",
"파일", "관리자", "탐색기", "클리너", "저장소", "계산기", "시계", "달력", "메모"
),
)
/**
* 패키지명, 이름, 시스템 카테고리를 종합하여 최적의 카테고리를 반환합니다.
*/
fun getFixedCategory(packageName: String?, appName: String?, systemCategory: Int): String {
// ---------------------------------------------------------
// [단계 1] 이전의 수동 맵(manualMapping) 확인
// ---------------------------------------------------------
manualMapping[packageName]?.let { return it }
// ---------------------------------------------------------
// [단계 2] 시스템 분류(systemCategory)가 명확한지 확인
// ---------------------------------------------------------
val systemType = when (systemCategory) {
ApplicationInfo.CATEGORY_GAME -> "GAME"
ApplicationInfo.CATEGORY_VIDEO -> "VIDEO"
ApplicationInfo.CATEGORY_MAPS -> "MAPS"
ApplicationInfo.CATEGORY_AUDIO -> "MUSIC"
ApplicationInfo.CATEGORY_PRODUCTIVITY -> "PRODUCTIVITY"
ApplicationInfo.CATEGORY_SOCIAL -> "SOCIAL"
ApplicationInfo.CATEGORY_NEWS -> "NEWS"
ApplicationInfo.CATEGORY_IMAGE -> "IMAGE"
else -> null
}
if (systemType != null) return systemType
// ---------------------------------------------------------
// [단계 3] 시스템 분류가 없는 경우 키워드 매핑 수행
// ---------------------------------------------------------
val pkg = packageName?.lowercase(Locale.ROOT) ?: ""
val name = appName?.lowercase(Locale.ROOT)?.replace(" ", "") ?: ""
for ((category, keywords) in keywordMapping) {
for (keyword in keywords) {
if (pkg.contains(keyword) || name.contains(keyword)) {
return category
}
}
}
return "UNDEFINED"
}
}

View File

@ -47,6 +47,7 @@ class AppInfoGetter : BaseGetter {
this.searchIndex = SimpleTransliterater.makeSearchIndex(appName) this.searchIndex = SimpleTransliterater.makeSearchIndex(appName)
this.pkgName = pkgName this.pkgName = pkgName
this.category = getCategory(ri.activityInfo.applicationInfo.category) this.category = getCategory(ri.activityInfo.applicationInfo.category)
this.systemCategoryInt = ri.activityInfo.applicationInfo.category
this.alphaCho = AlphabetToChosungMap.getCho(appName) this.alphaCho = AlphabetToChosungMap.getCho(appName)
this.appNameChosung = JamoUtils.split(appName).joinToString("") this.appNameChosung = JamoUtils.split(appName).joinToString("")
}) })