diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b44adb58..adeef5f6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,7 +55,9 @@
-
+
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
index 30c1a8b7..d0032e53 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
@@ -2,6 +2,7 @@ package bums.lunatic.launcher
import android.annotation.SuppressLint
import android.app.Activity
+import android.app.AppOpsManager
import android.app.SearchManager
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
@@ -9,11 +10,13 @@ import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
+import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
+import android.provider.Settings
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_UP
@@ -35,6 +38,7 @@ import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -62,6 +66,7 @@ import bums.lunatic.launcher.tokiz.Webtoons
import bums.lunatic.launcher.tokiz.YouTube
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
+import bums.lunatic.launcher.workers.WorkersDb.syncSystemUsageStats
import com.google.android.material.color.DynamicColors
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
@@ -126,8 +131,8 @@ open class LauncherActivity : CommonActivity() {
// 2. 더블 클릭 감지 (빈 공간)
override fun onDoubleTap(e: MotionEvent): Boolean {
// 더블 클릭 액션
- showToast("더블 클릭: 설정 열기")
- // 예: startActivity(Intent(this@LauncherActivity, SettingsActivity::class.java))
+// showToast("더블 클릭: 설정 열기")
+ openApp("org.telegram.messenger")
return true
}
@@ -142,7 +147,7 @@ open class LauncherActivity : CommonActivity() {
override fun onLongPress(e: MotionEvent) {
// 위젯 추가 메뉴 등을 띄우려면 여기서 처리
// 주의: 위젯 위에서 롱프레스하면 위젯 드래그가 먼저 작동하도록 설계해야 함
- showToast("바탕화면 롱프레스: 위젯 추가")
+// showToast("바탕화면 롱프레스: 위젯 추가")
selectWidget() // 기존에 만든 위젯 추가 함수 호출
}
@@ -156,13 +161,27 @@ open class LauncherActivity : CommonActivity() {
fun showToast(msg: String) {
android.widget.Toast.makeText(this, msg, android.widget.Toast.LENGTH_SHORT).show()
}
+ fun openApp(packageName: String) {
+ val intent = packageManager.getLaunchIntentForPackage(packageName)
+
+ if (intent != null) {
+ // 앱이 설치되어 있으면 실행
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ } else {
+ // 앱이 없으면 플레이스토어로 이동
+ val playStoreIntent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse("market://details?id=$packageName")
+ }
+ startActivity(playStoreIntent)
+ }
+ }
// 각 스와이프 동작 정의
fun onSwipeUp() {
- showAppDrawer() // 지난번에 만든 바텀시트 앱서랍 열기
+ openApp("com.google.android.apps.bard")
}
fun onSwipeDown() {
- // 알림창 내리기 등
try {
val service = getSystemService("statusbar")
val statusbarManager = Class.forName("android.app.StatusBarManager")
@@ -170,8 +189,14 @@ open class LauncherActivity : CommonActivity() {
expand.invoke(service)
} catch (e: Exception) { e.printStackTrace() }
}
- fun onSwipeLeft() { /* 페이지 이동 등 */ }
- fun onSwipeRight() { /* 페이지 이동 등 */ }
+
+ fun onSwipeLeft() {
+ showAppDrawer()
+ }
+
+ fun onSwipeRight() {
+ showContents(R.id.feeds)
+ }
private lateinit var binding: LauncherActivityBinding
@@ -543,7 +568,14 @@ open class LauncherActivity : CommonActivity() {
val intent = Intent(this, ForeGroundService::class.java)
this.startForegroundService(intent)
+// 1. 시스템 바 공간을 앱이 차지하도록 설정 (상태바 뒤로 레이아웃 확장)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ // 2. 상태바 색상을 투명하게 변경 (필요한 경우)
+ window.statusBarColor = Color.TRANSPARENT
+
+ // (선택 사항) 내비게이션 바도 투명하게 하고 싶다면
+ window.navigationBarColor = Color.TRANSPARENT
val nlService = Intent(this, NLService::class.java)
this.startService(nlService)
@@ -783,11 +815,11 @@ open class LauncherActivity : CommonActivity() {
// 손가락이 하나일 때만 이동 허용
// [추가] 삭제 영역과 겹치는지 확인하여 시각적 피드백 (예: 빨갛게 변함)
if (isViewOverlapping(currentDragView!!, binding.deleteZone)) {
- binding.deleteZone.setBackgroundColor(android.graphics.Color.RED)
- binding.deleteZone.text = "손을 떼면 삭제됩니다"
+ binding.deleteZone.setBackgroundResource(R.drawable.bg_circle_emoji_red)
+// binding.deleteZone.text = "\uD83D\uDDD1\uFE0F"
} else {
- binding.deleteZone.setBackgroundColor(0x99FF0000.toInt()) // 반투명 빨강 (원래 색)
- binding.deleteZone.text = "삭제하려면 여기에 놓으세요"
+ binding.deleteZone.setBackgroundResource(R.drawable.bg_circle_emoji)
+// binding.deleteZone.text = "\uD83D\uDDD1\uFE0F"
}
if (ev.pointerCount == 1 && lastTouchX != 0f && lastTouchY != 0f) {
@@ -866,7 +898,10 @@ open class LauncherActivity : CommonActivity() {
}
fun showContents(id : Int) {
+ binding.fragmentLayer.visibility = View.VISIBLE
binding.fragmentContainer.visibility = View.VISIBLE
+ binding.controllPanel.visibility = View.VISIBLE
+ binding.floatingActionMenu.visibility = View.VISIBLE
when(id) {
R.id.feeds -> {
supportFragmentManager.beginTransaction()
@@ -912,7 +947,10 @@ open class LauncherActivity : CommonActivity() {
supportFragmentManager.beginTransaction()
.remove(it)
.commit()
+ binding.fragmentLayer.visibility = View.GONE
binding.fragmentContainer.visibility = View.GONE
+ binding.controllPanel.visibility = View.GONE
+ binding.floatingActionMenu.visibility = View.GONE
}
}
@@ -962,10 +1000,31 @@ open class LauncherActivity : CommonActivity() {
super.onDestroy()
}
+ private fun checkUsageStatsPermission(): Boolean {
+ val appOps = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
+ val mode = appOps.checkOpNoThrow(
+ AppOpsManager.OPSTR_GET_USAGE_STATS,
+ android.os.Process.myUid(),
+ packageName
+ )
+ return mode == AppOpsManager.MODE_ALLOWED
+ }
+
+ private fun requestPermission() {
+ if (!checkUsageStatsPermission()) {
+ startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.O_MR1)
override fun onResume() {
super.onResume()
Blog.LOGE("LauncherActivity onResume")
+ if (checkUsageStatsPermission()) {
+ WorkersDb.syncSystemUsageStats(applicationContext)
+ } else {
+ requestPermission()
+ }
}
private fun openSearch() {
@@ -988,6 +1047,7 @@ open class LauncherActivity : CommonActivity() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
+ if (currentFragment == null) showContents(R.id.close)
when(currentFragment) {
is RssHome ->{
if (currentFragment.binding.layoutRssSummary.root.isVisible) {
@@ -1002,6 +1062,9 @@ open class LauncherActivity : CommonActivity() {
is Novels -> {
currentFragment.actionNextEvent(false)
}
+ else -> {
+ showContents(R.id.close)
+ }
}
}
})
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawer.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawer.kt
index 4c52e22c..a073a065 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawer.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawer.kt
@@ -48,6 +48,8 @@ import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.JamoUtils
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import io.realm.kotlin.ext.query
import io.realm.kotlin.query.RealmResults
@@ -99,7 +101,7 @@ class AppDrawer : CommonActivity() {
layoutType = settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0)
appsAdapter = AppsAdapter(packageManager!!, supportFragmentManager, binding.appsCount)
- contactAdapter = ContactAdapter(packageManager!!, supportFragmentManager)
+ contactAdapter = ContactAdapter(applicationContext, supportFragmentManager)
binding.appsCount.visibility = if (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) VISIBLE else GONE
binding.searchNmap.setOnClickListener {
@@ -244,7 +246,7 @@ class AppDrawer : CommonActivity() {
val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri)
pakage?.let {
mapIntent.setPackage(pakage)
- WorkersDb.updateAppUse(pakage)
+ WorkersDb.logAppUsage(pakage, UsageLogType.APP, UsageUpdateType.DATETIME)
}
startActivity(mapIntent)
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
index 3b1410b1..39fc3a5f 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
@@ -1,6 +1,7 @@
package bums.lunatic.launcher.apps
import android.app.Dialog
+import android.app.SearchManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -21,6 +22,8 @@ import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.JamoUtils
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.gms.common.wrappers.PackageManagerWrapper
import com.google.android.gms.common.wrappers.Wrappers.packageManager
@@ -110,12 +113,12 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
bottomSheet.layoutParams = layoutParams
}
}
- private var recAdapter: AppsAdapter? = null
+ private var recAdapter: RecommendedAppsAdapter? = null
private fun setupAdapters() {
// 기존 Activity의 packageManager 대신 requireContext().packageManager 사용
val pm = requireContext().packageManager
appsAdapter = AppsAdapter(pm, childFragmentManager, binding.appsCount)
- contactAdapter = ContactAdapter(pm, childFragmentManager)
+ contactAdapter = ContactAdapter(requireContext(), childFragmentManager)
// 가로 그리드 개수 4~5개 정도로 조정 (기존 2개였으면 그대로 유지)
binding.appsList.layoutManager = GridLayoutManager(context, 3,GridLayoutManager.HORIZONTAL,false)
@@ -126,7 +129,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
binding.contactList.layoutManager = GridLayoutManager(context, 3,GridLayoutManager.HORIZONTAL,false)
binding.contactList.adapter = contactAdapter
- recAdapter = AppsAdapter(pm, childFragmentManager,null)
+ recAdapter = RecommendedAppsAdapter(requireContext(),pm,childFragmentManager)
binding.recAppsList.layoutManager = GridLayoutManager(context, 1,GridLayoutManager.HORIZONTAL,false)
binding.recAppsList.adapter = recAdapter
}
@@ -163,7 +166,12 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
openSearchApps("https://www.youtube.com/results?search_query=${getInputText()}", "com.google.android.youtube")
}
binding.searchGoogle.setOnClickListener {
- openSearchApps("https://www.google.com/search?q=${getInputText()}", "com.android.chrome")
+ val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
+ putExtra(SearchManager.QUERY, getInputText()) // 질문 전달
+ }
+ if (intent.resolveActivity(requireContext().packageManager) != null) {
+ startActivity(intent)
+ }
}
binding.searchNaver.setOnClickListener {
openSearchApps("https://search.naver.com/search.naver?where=nexearch&query=${getInputText()}", "com.nhn.android.search")
@@ -178,7 +186,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(schemeString))
packageName?.let {
intent.setPackage(it)
- WorkersDb.updateAppUse(it)
+ WorkersDb.logAppUsage(packageName, UsageLogType.APP, UsageUpdateType.DATETIME)
}
startActivity(intent)
dismiss() // 실행 후 닫기
@@ -211,13 +219,24 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val pm = requireContext().packageManager
// 1. [추천 로직] 에러가 나도 앱 목록 로딩은 진행되도록 try-catch 분리
- val recommendedPkgNames = try {
- if (keyword.isNullOrEmpty()) {
- WorkersDb.getContextualRecommendations(limit = 5)
- } else {
- emptyList() // 검색 중에는 추천 안 함
+ val scoredItems = WorkersDb.getContextualRecommendations(limit = 8)
+
+ val unifiedList = mutableListOf()
+
+ for (item in scoredItems) {
+ if (item.type == "APP") {
+ val app = realm.query("pkgName == $0", item.key).first().find()
+ if (app != null && !app.blockRecommend) {
+ unifiedList.add(RecommendationItem.AppItem(realm.copyFromRealm(app)))
+ }
+ } else if (item.type == "CONTACT") {
+ // 연락처 ID나 전화번호로 조회 (Log 저장 시 key가 무엇인지에 따라 다름)
+ val contact = realm.query("id == $0", item.key).first().find()
+ if (contact != null) {
+ unifiedList.add(RecommendationItem.ContactItem(realm.copyFromRealm(contact)))
+ }
}
- } catch (e: Exception) { emptyList() }
+ }
try {
// 2. [쿼리 구성]
@@ -254,12 +273,9 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
false
}
}
-// C. [추천 앱 객체 추출]
- val recAppList = recommendedPkgNames.mapNotNull { pkg ->
- allApps.find { it.pkgName == pkg }
- }
- val mainAppList = allApps
+
+ val mainAppList = allApps.filter { it.visibilityMode == 0}
var contactQuery = realm.query()
@@ -282,16 +298,15 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val contactsResult = contactQuery.find()
val contactsList = contactsResult.map { realm.copyFromRealm(it) }
+ Blog.LOGE("unifiedList >>> ${unifiedList.size}")
+
// 6. [UI 업데이트]
withContext(Dispatchers.Main) {
- if (recAppList.isNotEmpty() && keyword.isNullOrEmpty()) {
+
+ if (unifiedList.isNotEmpty()) {
binding.titleRecommend.visibility = View.VISIBLE
binding.recAppsList.visibility = View.VISIBLE
- recAdapter?.updateData(recAppList)
- } else {
- // 검색 중이거나 데이터 없으면 숨김
- binding.titleRecommend.visibility = View.GONE
- binding.recAppsList.visibility = View.GONE
+ recAdapter?.submitList(unifiedList)
}
// 2. 전체 앱 리스트 업데이트
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppMenu.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppMenu.kt
index 20b339e5..bbacf2cf 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppMenu.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppMenu.kt
@@ -54,6 +54,8 @@ import bums.lunatic.launcher.helpers.UniUtils.Companion.screenHeight
import bums.lunatic.launcher.helpers.UniUtils.Companion.screenWidth
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.utils.JamoUtils
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -126,29 +128,20 @@ internal class AppMenu : BottomSheetDialogFragment() {
"최종 실행 일시 : ".plus(SimpleDateFormat("yyyy-MM-dd HH:mm").format(Date(app.lastUseDate)))
app.currentInstalled = true
binding.alterName.setText(app.koreanName)
-// app.clickCount = app.clickCount + 15
-
-// app.lastUseDate = Math.max(app.lastUseDate, System.currentTimeMillis())
-// app.clickCount = app.clickCount + 15
-// app.lastUseDate = Math.max(app.lastUseDate, System.currentTimeMillis())
+ binding.recommend.isChecked = app.blockRecommend
+ binding.listVisible.isChecked = app.visibilityMode == 1
}
}
}
}
- fun update() {
- WorkersDb.getRealm().apply {
- writeBlocking {
- var result = query("pkgName == $0",packageName).find()
- if(result.size > 0) {
- val app = result.first()
- app.clickCount = app.clickCount + 15
- }
- }
- }
+
+ binding.totalTouch.setOnClickListener {
+ WorkersDb.logAppUsage(packageName, UsageLogType.APP,UsageUpdateType.COUNT)
+ }
+ binding.lastTouchDate.setOnClickListener {
+ WorkersDb.logAppUsage(packageName, UsageLogType.APP,UsageUpdateType.DATETIME)
}
- binding.totalTouch.setOnClickListener { update() }
- binding.lastTouchDate.setOnClickListener { update() }
binding.alterName.doOnTextChanged { text, start, before, count ->
WorkersDb.getRealm().apply {
@@ -168,6 +161,7 @@ internal class AppMenu : BottomSheetDialogFragment() {
hint = defAppName
}
+
binding.appPackage.text = packageName
return binding.root
}
@@ -189,6 +183,25 @@ internal class AppMenu : BottomSheetDialogFragment() {
binding.appInfo.setOnClickListener { appInfo() }
binding.appShare.setOnClickListener { share() }
binding.appUninstall.setOnClickListener { uninstall() }
+ binding.listVisible.setOnClickListener {
+ WorkersDb.getRealm().writeBlocking {
+ var result = query("pkgName == $0",packageName).find()
+ if (result.size > 0) {
+ var appInfo = result.first()
+ appInfo.visibilityMode = if (binding.listVisible.isChecked) 1 else 0
+ }
+
+ }
+ }
+ binding.recommend.setOnClickListener {
+ WorkersDb.getRealm().writeBlocking {
+ var result = query("pkgName == $0",packageName).find()
+ if(result.size > 0){
+ var appInfo = result.first()
+ appInfo.blockRecommend = binding.recommend.isChecked
+ }
+ }
+ }
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppsAdapter.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppsAdapter.kt
index bbc8c0f3..d7e8114d 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppsAdapter.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppsAdapter.kt
@@ -30,21 +30,52 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.R
-import bums.lunatic.launcher.apps.IconPackManager.Companion.getDrawableIconForPackage
import bums.lunatic.launcher.databinding.AppsChildBinding
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.utils.Blog
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import io.realm.kotlin.ext.query
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.async
import kotlinx.coroutines.launch
+class AppsViewHolder(var view: AppsChildBinding) : RecyclerView.ViewHolder(view.root) {
+ fun bind(
+ packageManager: PackageManager,
+ fragmentManager: FragmentManager,
+ appinfo: AppInfo) {
+ view.apply {
+ childTextview.text = appinfo.appName
+ appIconTwo.visibility = View.VISIBLE
+ loadIconAsync(appIconTwo, appinfo.pkgName)
+ childTextview.apply {
+ gravity = Gravity.CENTER
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twelve))
+ }
+ }
+
+ view.root.apply {
+ /* on click - open app */
+ setOnClickListener {
+ appinfo.pkgName?.let { WorkersDb.logAppUsage(it, UsageLogType.APP,datetime = UsageUpdateType.DATETIME) }
+ context.startActivity(packageManager.getLaunchIntentForPackage(appinfo.pkgName!!))
+ }
+
+ /* on long click - open app menu */
+ setOnLongClickListener {
+ appinfo.pkgName?.let { WorkersDb.logAppUsage(it, UsageLogType.APP,datetime = UsageUpdateType.JC) }
+ AppMenu().apply {
+ }.show(fragmentManager, appinfo.pkgName)
+ true
+ }
+ }
+ }
+}
internal class AppsAdapter(
private val packageManager: PackageManager,
private val fragmentManager: FragmentManager,
- private val appsCount: TextView?) : RecyclerView.Adapter() {
+ private val appsCount: TextView?) : RecyclerView.Adapter() {
private var oldList = mutableListOf()
// private var appGravity: Int = Gravity.CENTER
@@ -58,94 +89,18 @@ internal class AppsAdapter(
override fun onBindViewHolder(holder: AppsViewHolder, i: Int) {
val item = oldList[i]
-
-
-
-
- holder.view.apply {
- childTextview.text = item.appName
- appIconTwo.visibility = View.VISIBLE
- loadIconAsync(appIconTwo, item.pkgName)
- childTextview.apply {
- gravity = Gravity.CENTER
- setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twelve))
- }
- }
-
- holder.view.root.apply {
- /* on click - open app */
- setOnClickListener {
- WorkersDb.getRealm().apply {
- writeBlocking {
- Blog.LOGE("item.pkgName >>>> ${item.pkgName
- }")
- var result = query("pkgName == $0",item.pkgName).find()
- if(result.size > 0) {
- val app = result.first()
- app.clickCount = app.clickCount + 1
- app.lastUseDate = Math.max(app.lastUseDate, System.currentTimeMillis())
- }
- }
- }
- item.pkgName?.let { WorkersDb.logAppUsage(it,"APP") }
- context.startActivity(packageManager.getLaunchIntentForPackage(item.pkgName!!))
- }
-
- /* on long click - open app menu */
- setOnLongClickListener {
- WorkersDb.getRealm().apply {
- writeBlocking {
- var result = query("pkgName == $0",item.pkgName).find()
- if(result.size > 0) {
- val app = result.first()
- app.clickCount = app.clickCount + 15
-// app.lastUseDate = Math.max(app.lastUseDate, System.currentTimeMillis())
- }
- }
- }
- AppMenu().apply {
- }.show(fragmentManager, item.pkgName)
- true
- }
- }
+ holder.bind(packageManager,fragmentManager,item)
}
- private fun loadIconAsync(imageView: android.widget.ImageView, pkgName: String?) {
- if (pkgName == null) return
- // 1. 이미지가 로딩되기 전 초기화 (재사용 뷰 깜빡임 방지)
- imageView.setImageDrawable(null)
-
- // Tag를 사용하여 뷰홀더가 재사용되었을 때 이전 작업 취소 식별
- imageView.tag = pkgName
-
- // CoroutineScope (lifecycleScope나 adapter 내부 scope 사용)
- kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch {
- val icon = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
- IconPackManager.getDrawableIconForPackage(imageView.context, pkgName)
- }
-
- // 로딩이 끝났는데 뷰가 여전히 같은 앱을 가리키고 있는지 확인
- if (imageView.tag == pkgName) {
- imageView.setImageDrawable(icon)
- }
- }
- }
override fun getItemCount(): Int = oldList.size
- inner class AppsViewHolder(var view: AppsChildBinding) : RecyclerView.ViewHolder(view.root)
+
/* update app list */
fun updateData(newList: List) {
val diffUtilResult = DiffUtil.calculateDiff(AppsDiffUtil(oldList, newList))
-
- // [수정 전] dispatchUpdatesTo가 먼저 있어서 에러 발생함
- // diffUtilResult.dispatchUpdatesTo(this)
- // oldList.clear()
- // oldList.addAll(newList)
-
- // [수정 후] 반드시 리스트 데이터를 먼저 갱신하고 나서 알림을 보내야 합니다!
oldList.clear()
oldList.addAll(newList)
diffUtilResult.dispatchUpdatesTo(this) // <-- 순서 변경 (맨 뒤로)
@@ -171,3 +126,25 @@ internal class AppsDiffUtil(
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
}
+
+fun loadIconAsync(imageView: android.widget.ImageView, pkgName: String?) {
+ if (pkgName == null) return
+
+ // 1. 이미지가 로딩되기 전 초기화 (재사용 뷰 깜빡임 방지)
+ imageView.setImageDrawable(null)
+
+ // Tag를 사용하여 뷰홀더가 재사용되었을 때 이전 작업 취소 식별
+ imageView.tag = pkgName
+
+ // CoroutineScope (lifecycleScope나 adapter 내부 scope 사용)
+ kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch {
+ val icon = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
+ IconPackManager.getDrawableIconForPackage(imageView.context, pkgName)
+ }
+
+ // 로딩이 끝났는데 뷰가 여전히 같은 앱을 가리키고 있는지 확인
+ if (imageView.tag == pkgName) {
+ imageView.setImageDrawable(icon)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactAdapter.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactAdapter.kt
index 8b34ca28..b0216a1b 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactAdapter.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactAdapter.kt
@@ -19,7 +19,9 @@
package bums.lunatic.launcher.apps
import android.annotation.SuppressLint
-import android.content.pm.PackageManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
import android.view.Gravity
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -28,14 +30,42 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import bums.lunatic.launcher.databinding.ContactItemBinding
import bums.lunatic.launcher.model.SimpleContact
-import bums.lunatic.launcher.utils.JamoUtils
-import io.realm.kotlin.types.RealmObject
-import io.realm.kotlin.types.annotations.PrimaryKey
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
+import bums.lunatic.launcher.workers.WorkersDb
+class ContactViewHolder(var view: ContactItemBinding) : RecyclerView.ViewHolder(view.root) {
+ fun bind(context: Context, fragmentManager: FragmentManager , simpleContact: SimpleContact) {
+ view.apply {
+ name.text = simpleContact.name
+ number.text= simpleContact.phoneNumber
+ }
+ view.root.apply {
+ /* on click - open app */
+ setOnClickListener {
+ var intent = Intent(Intent.ACTION_DIAL);
+ intent.setData(Uri.parse("tel:" + simpleContact.phoneNumber));
+ context.startActivity(intent);
+ simpleContact.id?.let {
+ WorkersDb.logAppUsage(it, UsageLogType.CONTACT, UsageUpdateType.DATETIME)
+ }
+ }
+
+ /* on long click - open app menu */
+ setOnLongClickListener {
+ simpleContact.id?.let {
+ WorkersDb.logAppUsage(it, UsageLogType.CONTACT, UsageUpdateType.JC)
+ }
+ ContactMenu().show(fragmentManager, simpleContact.id.toString())
+ true
+ }
+ }
+ }
+ }
internal class ContactAdapter (
- private val packageManager: PackageManager,
- private val fragmentManager: FragmentManager) : RecyclerView.Adapter() {
+ private val context: Context,
+ private val fragmentManager: FragmentManager) : RecyclerView.Adapter() {
private var oldList = mutableListOf()
private var appGravity: Int = Gravity.CENTER
@@ -49,32 +79,12 @@ internal class ContactAdapter (
override fun onBindViewHolder(holder: ContactViewHolder, i: Int) {
val item = oldList[i]
-// BLog.LOGE("name >>> ${item.name} :: ${item.touchCount} :: ${RecentCallGetter.dateFormat.format(
-// Date(item.lastedTouchDateTime)
-// )}")
-
- holder.view.apply {
- name.text = item.name
- number.text= item.phoneNumber
- }
- holder.view.root.apply {
- /* on click - open app */
- setOnClickListener {
- ContactMenu().show(fragmentManager, item.id.toString())
- }
-
- /* on long click - open app menu */
- setOnLongClickListener {
-// BLog.LOGE("item.id.toString() >> ${item.id.toString()}")
- ContactMenu().show(fragmentManager, item.id.toString())
- true
- }
- }
+ holder.bind(context, fragmentManager,item)
}
override fun getItemCount(): Int = oldList.size
- inner class ContactViewHolder(var view: ContactItemBinding) : RecyclerView.ViewHolder(view.root)
+
/* update app list */
fun updateData(newList: List) {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactMenu.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactMenu.kt
index 63c8b049..cb43abc4 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactMenu.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/ContactMenu.kt
@@ -29,6 +29,8 @@ import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.databinding.ContactMenuBinding
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
+import bums.lunatic.launcher.workers.UsageLogType
+import bums.lunatic.launcher.workers.UsageUpdateType
import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -62,19 +64,14 @@ internal class ContactMenu : BottomSheetDialogFragment() {
}
}
- fun update() {
- WorkersDb.getRealm().writeBlocking {
- if (contactId != null && contactId.length ?: 0 > 0) {
- val result = query().query("id == $0", contactId).find()
- if(result.size > 0){
- var contact = result.first()
- contact.touchCount = contact.touchCount + 15
- }
- }
+
+
+ binding.totalTouch.setOnClickListener {
+ WorkersDb.logAppUsage(contactId, UsageLogType.CONTACT,UsageUpdateType.COUNT)
}
+ binding.lastTouchDate.setOnClickListener {
+ WorkersDb.logAppUsage(contactId, UsageLogType.CONTACT,UsageUpdateType.DATETIME)
}
- binding.totalTouch.setOnClickListener { update() }
- binding.lastTouchDate.setOnClickListener { update() }
val resolver = lActivity!!.contentResolver
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
@@ -84,23 +81,23 @@ internal class ContactMenu : BottomSheetDialogFragment() {
)
Blog.LOGE("GetContact", "packageName ${contactId}")
- try {
- val cursor = resolver.query(phoneUri, projection, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null , null)
- if (cursor != null) {
- while (cursor.moveToNext()) {
- val nameIndex = cursor.getColumnIndex(projection[0])
- val numberIndex = cursor.getColumnIndex(projection[1])
- contactName = cursor.getString(nameIndex)
- var number = cursor.getString(numberIndex)
- contactPhoneNumber = number.replace("-", "")
- Blog.LOGE("GetContact", "이름 : $contactName 번호 : $contactPhoneNumber ")
- }
- }
- // 데이터 계열은 반드시 닫아줘야 한다.
- cursor!!.close()
- } catch ( e : Exception) {
- e.printStackTrace()
- }
+ try {
+ val cursor = resolver.query(phoneUri, projection, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null , null)
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ val nameIndex = cursor.getColumnIndex(projection[0])
+ val numberIndex = cursor.getColumnIndex(projection[1])
+ contactName = cursor.getString(nameIndex)
+ var number = cursor.getString(numberIndex)
+ contactPhoneNumber = number.replace("-", "")
+ Blog.LOGE("GetContact", "이름 : $contactName 번호 : $contactPhoneNumber ")
+ }
+ }
+ // 데이터 계열은 반드시 닫아줘야 한다.
+ cursor!!.close()
+ } catch ( e : Exception) {
+ e.printStackTrace()
+ }
/* get application info */
@@ -121,6 +118,28 @@ internal class ContactMenu : BottomSheetDialogFragment() {
binding.detailedInfo.setOnClickListener { detailedInfo() }
binding.call.setOnClickListener { callPhone() }
binding.sms.setOnClickListener { sendSms() }
+ binding.listVisible.setOnClickListener {
+ if (contactId != null && contactId.length ?: 0 > 0) {
+ WorkersDb.getRealm().writeBlocking {
+ val result = query().query("id == $0", contactId).find()
+ if (result.size > 0) {
+ var contact = result.first()
+ contact.visibilityMode = if (binding.listVisible.isChecked) 1 else 0
+ }
+ }
+ }
+ }
+ binding.recommend.setOnClickListener {
+ if (contactId != null && contactId.length ?: 0 > 0) {
+ WorkersDb.getRealm().writeBlocking {
+ val result = query().query("id == $0", contactId).find()
+ if(result.size > 0){
+ var contact = result.first()
+ contact.blockRecommend = binding.recommend.isChecked
+ }
+ }
+ }
+ }
// binding.activityBrowser.setOnClickListener { activityBrowser() }
// binding.appStore.setOnClickListener { appStore() }
// binding.appFreeform.setOnClickListener { freeform() }
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/RecommendedAppsAdapter.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/RecommendedAppsAdapter.kt
index 9e3fe1e3..d8982c5d 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/RecommendedAppsAdapter.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/RecommendedAppsAdapter.kt
@@ -1,68 +1,68 @@
package bums.lunatic.launcher.apps
+import android.content.Context
import android.content.pm.PackageManager
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
-import bums.lunatic.launcher.databinding.AppsChildBinding // 기존 레이아웃 재사용 (또는 별도 레이아웃 생성)
+import bums.lunatic.launcher.databinding.AppsChildBinding
import bums.lunatic.launcher.databinding.AppsChildRecBinding
+import bums.lunatic.launcher.databinding.ContactItemBinding
import bums.lunatic.launcher.model.AppInfo
-import bums.lunatic.launcher.workers.WorkersDb
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import bums.lunatic.launcher.model.SimpleContact
-class RecommendedAppsAdapter(private val pm: PackageManager) : RecyclerView.Adapter() {
+sealed class RecommendationItem {
+ // 앱을 감싸는 클래스
+ data class AppItem(val appInfo: AppInfo) : RecommendationItem()
- private val items = ArrayList()
+ // 연락처를 감싸는 클래스
+ data class ContactItem(val contact: SimpleContact) : RecommendationItem()
+}
- fun submitList(newItems: List) {
+
+class RecommendedAppsAdapter(
+ private val context: Context,
+ private val packageManager: PackageManager,
+ private val fragmentManager: FragmentManager) : RecyclerView.Adapter() {
+
+ private val items = ArrayList()
+
+ fun submitList(newItems: List) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- // 기존 apps_child.xml을 재사용하거나, 아이콘만 보여주는 새로운 xml을 만들어도 됨
- val binding = AppsChildRecBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return ViewHolder(binding)
+ companion object {
+ const val TYPE_APP = 0
+ const val TYPE_CONTACT = 1
}
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val item = items[position]
-
- // 앱 이름 (추천 영역에서는 숨기거나 작게 표시 가능)
- holder.binding.childTextview.text = item.appName
- // holder.binding.appName.visibility = View.GONE // 이름 숨기고 싶으면 주석 해제
-
- // 아이콘 비동기 로딩 (이전 성능 최적화 코드 적용)
- holder.binding.appIconTwo.setImageDrawable(null)
- holder.binding.appIconTwo.tag = item.pkgName
-
- CoroutineScope(Dispatchers.Main).launch {
- val icon = withContext(Dispatchers.IO) {
- IconPackManager.getDrawableIconForPackage(holder.binding.root.context, item.pkgName ?: "")
- }
- if (holder.binding.appIconTwo.tag == item.pkgName) {
- holder.binding.appIconTwo.setImageDrawable(icon)
- }
+ override fun getItemViewType(position: Int): Int {
+ return when (items[position]) {
+ is RecommendationItem.AppItem -> TYPE_APP
+ is RecommendationItem.ContactItem -> TYPE_CONTACT
}
+ }
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return (if (viewType == TYPE_APP) {
+ AppsViewHolder(AppsChildBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ } else {
+ ContactViewHolder(ContactItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }) as RecyclerView.ViewHolder
+ }
- // 클릭 이벤트
- holder.itemView.setOnClickListener {
- try {
- val intent = pm.getLaunchIntentForPackage(item.pkgName ?: "")
- if (intent != null) {
- holder.itemView.context.startActivity(intent)
- // [중요] 사용 로그 저장 -> 추천 정확도 상승
- WorkersDb.logAppUsage(item.pkgName ?: "", "APP")
- }
- } catch (e: Exception) { e.printStackTrace() }
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ when (val item = items[position]) {
+ is RecommendationItem.AppItem -> {
+ (holder as AppsViewHolder).bind(packageManager,fragmentManager,item.appInfo)
+ }
+ is RecommendationItem.ContactItem -> {
+ (holder as ContactViewHolder).bind(context, fragmentManager,item.contact)
+ }
}
}
override fun getItemCount(): Int = items.size
-
- class ViewHolder(val binding: AppsChildRecBinding) : RecyclerView.ViewHolder(binding.root)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt
index 9f21ff2c..90528599 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt
@@ -127,19 +127,12 @@ class LocationUpdateService : Service(), LocationListener {
}
}
-
-
-
protected var locationManager: LocationManager? = null
var checkGPS = false
var checkNetwork = false
- // boolean canGetLocation = false;
- var loc: Location? = null
-
-
override fun onBind(intent: Intent?): IBinder? {
- TODO("Not yet implemented")
+ return null
}
override fun onLocationChanged(p0: Location) {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt
index c86a2c12..137fc750 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt
@@ -43,6 +43,8 @@ object TaskAggregator {
if (oldApp != null) {
newApp.clickCount = oldApp.clickCount
newApp.lastUseDate = oldApp.lastUseDate
+ newApp.visibilityMode = oldApp.visibilityMode
+ newApp.blockRecommend = oldApp.blockRecommend
// 즐겨찾기 여부 등 보존해야 할 다른 필드가 있다면 여기서 복사
// newApp.isFavorite = oldApp.isFavorite
}
@@ -75,16 +77,18 @@ object TaskAggregator {
if (oldContact != null) {
contact.touchCount = oldContact.touchCount
contact.lastedTouchDateTime = oldContact.lastedTouchDateTime
+ contact.visibilityMode = oldContact.visibilityMode
+ contact.blockRecommend = oldContact.blockRecommend
}
copyToRealm(contact, UpdatePolicy.ALL)
}
// 삭제된 연락처 처리
- val contactsToDelete = query(SimpleContact::class).find().filter {
- !activeContactIds.contains(it.id)
- }
- contactsToDelete.forEach { delete(it) }
+// val contactsToDelete = query(SimpleContact::class).find().filter {
+// !activeContactIds.contains(it.id)
+// }
+// contactsToDelete.forEach { delete(it) }
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
index 52d60283..fd930ed1 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
@@ -1,5 +1,7 @@
package bums.lunatic.launcher.workers
+import android.app.usage.UsageStatsManager
+import android.content.Context
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.common.letTrue
import bums.lunatic.launcher.model.AppInfo
@@ -57,6 +59,10 @@ class CustMigration : AutomaticSchemaMigration {
Blog.LOGE(migrationContext.newRealm.configuration.schemaVersion.toString())
}
}
+
+data class ScoredItem(val key: String, val type: String, val score: Double)
+enum class UsageLogType { APP, CONTACT };
+enum class UsageUpdateType { JC, COUNT, DATETIME };
object WorkersDb {
//RecentCall::class, RecentSms::class,
@@ -70,30 +76,73 @@ object WorkersDb {
)
//,UserActionModel::class
+
+
// [추가] 앱/연락처 사용 시 로그 저장 (기존 updateAppUse 대신 이거 호출)
- fun logAppUsage(key: String, type: String = "APP") {
+ fun logAppUsage(key: String, type: UsageLogType = UsageLogType.APP, datetime: UsageUpdateType) {
val realm = getRealm()
val calendar = Calendar.getInstance()
- realm.writeBlocking { // 비동기로 하려면 write { } 사용
- // 1. 기존 카운트 증가 (기존 로직 유지)
- // ... (AppInfo 조회 후 clickCount++ 하는 코드) ...
+ realm.writeBlocking {
+ when(type) {
+ UsageLogType.APP -> {
+ var result = query("pkgName == $0",key).find()
+ if(result.isNotEmpty()) {
+ val app = result.first()
+ when(datetime) {
+ UsageUpdateType.JC -> {
+ app.clickCount += 1
+ }
+ UsageUpdateType.COUNT -> {
+ app.clickCount += 15
+ }
+ UsageUpdateType.DATETIME -> {
+ app.clickCount += 1
+ app.lastUseDate = System.currentTimeMillis()
+ }
+ else -> {}
+ }
- // 2. 상세 로그 저장 (추가된 부분)
- copyToRealm(AppUsageLog().apply {
- itemKey = key
- itemType = type
- timestamp = System.currentTimeMillis()
- month = calendar.get(Calendar.MONTH)
- dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
- dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
- hour = calendar.get(Calendar.HOUR_OF_DAY)
- })
+ }
+ }
+ UsageLogType.CONTACT -> {
+ val results = query().query("id == $0", key).find()
+ if(results.isNotEmpty()) {
+ val result = results.first()
+ when(datetime) {
+ UsageUpdateType.JC -> {
+ result.touchCount += 1
+ }
+ UsageUpdateType.COUNT -> {
+ result.touchCount += 15
+ }
+ UsageUpdateType.DATETIME -> {
+ result.touchCount += 1
+ result.lastedTouchDateTime = System.currentTimeMillis()
+ }
+ else -> {}
+ }
+ }
+ }
+ else -> {}
+ }
+
+ if (datetime.equals(UsageUpdateType.JC) == false) {
+ copyToRealm(AppUsageLog().apply {
+ itemKey = key
+ itemType = type.name
+ timestamp = System.currentTimeMillis()
+ month = calendar.get(Calendar.MONTH)
+ dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
+ dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
+ hour = calendar.get(Calendar.HOUR_OF_DAY)
+ })
+ }
}
}
// [핵심] 현재 시간 상황에 맞는 추천 리스트 가져오기
- fun getContextualRecommendations(limit: Int = 5): List {
+ fun getContextualRecommendations(limit: Int = 8): List {
val realm = getRealm()
val calendar = Calendar.getInstance()
@@ -103,13 +152,14 @@ object WorkersDb {
val curHour = calendar.get(Calendar.HOUR_OF_DAY)
// 최근 3개월 데이터만 조회 (너무 오래된 데이터는 노이즈가 됨)
- val threeMonthsAgo = System.currentTimeMillis() - (90L * 24 * 60 * 60 * 1000)
+ val threeMonthsAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000)
// 쿼리: 최근 데이터만 가져와서 메모리에서 계산 (복잡한 가중치는 메모리 연산이 빠름)
val logs = realm.query("timestamp > $0", threeMonthsAgo).find()
- // 점수 계산
val scores = HashMap()
+ val types = HashMap()
+ // 점수 계산
for (log in logs) {
var score = 1.0 // 기본 점수 (최근에 썼다는 것 자체로 의미 있음)
@@ -134,13 +184,17 @@ object WorkersDb {
// 최종 점수 누적
val finalScore = score * decay
scores[log.itemKey] = (scores[log.itemKey] ?: 0.0) + finalScore
+ types[log.itemKey] = log.itemType
}
// 점수 높은 순으로 정렬하여 상위 N개 반환
return scores.entries
.sortedByDescending { it.value }
.take(limit)
- .map { it.key }
+ .map {
+ // 키, 타입, 점수를 묶어서 반환
+ ScoredItem(it.key, types[it.key] ?: "APP", it.value)
+ }
}
val schemaVersion : Long = BuildConfig.BuildDateTime
@@ -274,17 +328,62 @@ object WorkersDb {
}
}
- fun updateAppUse(pkg : String) {
- getRealm().writeBlocking {
- val result = query().query("pkgName == $0",pkg).find()
- if(result.size > 0) {
- val appInfo = result.first()
- appInfo.clickCount = appInfo.clickCount + 1
- appInfo.lastUseDate = System.currentTimeMillis()
+ fun syncSystemUsageStats(context: Context) {
+ val usm = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
+ val calendar = Calendar.getInstance()
+ val endTime = calendar.timeInMillis
+ val startTime = endTime - (1000 * 60 * 60 * 24) // 최근 24시간 조회
+
+ // 1. 시스템에서 사용 기록 조회
+ val usageStatsList = usm.queryUsageStats(
+ UsageStatsManager.INTERVAL_DAILY,
+ startTime,
+ endTime
+ )
+
+ if (usageStatsList.isNullOrEmpty()) return
+
+ val realm = getRealm() // WorkersDb의 getRealm() 활용
+
+ realm.writeBlocking {
+ for (stats in usageStatsList) {
+ val pkgName = stats.packageName
+ val lastUsedTime = stats.lastTimeUsed
+
+ // 2. 우리 DB(AppInfo)에 저장된 시간보다 최신인지 확인
+ val appInfo = query("pkgName == $0", pkgName).first().find()
+
+ if (appInfo != null) {
+ // 시스템 기록이 내 DB 기록보다 더 최신이면 -> 런처 밖에서 실행된 것임!
+ if (lastUsedTime > appInfo.lastUseDate) {
+
+ // A. AppInfo 갱신
+ appInfo.lastUseDate = lastUsedTime
+ // (선택) 외부 실행도 카운트에 포함할지 결정
+ appInfo.clickCount += 1
+
+ // B. AppUsageLog에 로그 추가 (외부 실행 로그)
+ // 주의: 너무 많은 로그가 쌓일 수 있으므로 중복 체크 필요
+ val existingLog = query(
+ "itemKey == $0 AND timestamp == $1",
+ pkgName, lastUsedTime
+ ).find()
+
+ if (existingLog.isEmpty()) {
+ copyToRealm(AppUsageLog().apply {
+ itemKey = pkgName
+ itemType = "APP"
+ timestamp = lastUsedTime
+ // month, day 등 날짜 정보 계산해서 넣기...
+ })
+ }
+ }
+ }
}
}
}
+
fun push(loc: LocationLog) {
getRealm().writeBlocking {
try {
diff --git a/app/src/main/res/drawable/bg_circle_emoji.xml b/app/src/main/res/drawable/bg_circle_emoji.xml
new file mode 100644
index 00000000..19afacf8
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_emoji.xml
@@ -0,0 +1,7 @@
+
+ -
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_circle_emoji_red.xml b/app/src/main/res/drawable/bg_circle_emoji_red.xml
new file mode 100644
index 00000000..0f8bfae3
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_emoji_red.xml
@@ -0,0 +1,7 @@
+
+ -
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml
new file mode 100644
index 00000000..8609d035
--- /dev/null
+++ b/app/src/main/res/drawable/search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/app_menu.xml b/app/src/main/res/layout/app_menu.xml
index 1b4fde4c..fee72d2d 100644
--- a/app/src/main/res/layout/app_menu.xml
+++ b/app/src/main/res/layout/app_menu.xml
@@ -33,6 +33,7 @@
android:padding="@dimen/eight"
android:inputType="textNoSuggestions"
/>
+
+
+
+
+ app:layout_constraintRight_toLeftOf="@+id/search_google"/>
+
+
-
-
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/listVisible" />
-
-
+
-
+
diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml
index 7b940da8..11811a46 100644
--- a/app/src/main/res/layout/launcher_activity.xml
+++ b/app/src/main/res/layout/launcher_activity.xml
@@ -20,86 +20,95 @@
app:layout_constraintEnd_toEndOf="parent" />
-
-
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" >
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+