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" > + + + + + - - - - - - + + + +