This commit is contained in:
lunaticbum 2026-01-08 18:20:30 +09:00
parent dc6b95108f
commit 62f1e646ab
44 changed files with 1848 additions and 1175 deletions

View File

@ -101,6 +101,11 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|layoutDirection|navigation"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.WEB_SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
@ -117,6 +122,7 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- <activity-->

View File

@ -14,6 +14,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_BUTTON_A
@ -38,6 +39,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.apps.AppDrawerBottomSheet
import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.LauncherActivityBinding
import bums.lunatic.launcher.feeds.WidgetHost
@ -79,6 +81,97 @@ import java.util.Date
open class LauncherActivity : CommonActivity() {
// LauncherActivity 내부 (inner class)
inner class HomeGestureListener : GestureDetector.SimpleOnGestureListener() {
// 1. 스와이프 감지 (상하좌우)
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (e1 == null) return false
val diffY = e2.y - e1.y
val diffX = e2.x - e1.x
val SWIPE_THRESHOLD = 100
val SWIPE_VELOCITY_THRESHOLD = 100
if (Math.abs(diffX) > Math.abs(diffY)) {
// 좌우 스와이프
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight()
} else {
onSwipeLeft()
}
return true
}
} else {
// 상하 스와이프
if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
if (diffY > 0) {
onSwipeDown()
} else {
onSwipeUp()
}
return true
}
}
return false
}
// 2. 더블 클릭 감지 (빈 공간)
override fun onDoubleTap(e: MotionEvent): Boolean {
// 더블 클릭 액션
showToast("더블 클릭: 설정 열기")
// 예: startActivity(Intent(this@LauncherActivity, SettingsActivity::class.java))
return true
}
// 3. 싱글 클릭 감지 (빈 공간)
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
// 싱글 클릭 액션 (필요하다면)
// showToast("빈 공간 클릭됨")
return true
}
// 4. 롱프레스 감지 (빈 공간)
override fun onLongPress(e: MotionEvent) {
// 위젯 추가 메뉴 등을 띄우려면 여기서 처리
// 주의: 위젯 위에서 롱프레스하면 위젯 드래그가 먼저 작동하도록 설계해야 함
showToast("바탕화면 롱프레스: 위젯 추가")
selectWidget() // 기존에 만든 위젯 추가 함수 호출
}
// onDown은 true를 반환해야 다른 제스처들이 시작됨
override fun onDown(e: MotionEvent): Boolean {
return true
}
}
// (편의용) 토스트 함수
fun showToast(msg: String) {
android.widget.Toast.makeText(this, msg, android.widget.Toast.LENGTH_SHORT).show()
}
// 각 스와이프 동작 정의
fun onSwipeUp() {
showAppDrawer() // 지난번에 만든 바텀시트 앱서랍 열기
}
fun onSwipeDown() {
// 알림창 내리기 등
try {
val service = getSystemService("statusbar")
val statusbarManager = Class.forName("android.app.StatusBarManager")
val expand = statusbarManager.getMethod("expandNotificationsPanel")
expand.invoke(service)
} catch (e: Exception) { e.printStackTrace() }
}
fun onSwipeLeft() { /* 페이지 이동 등 */ }
fun onSwipeRight() { /* 페이지 이동 등 */ }
private lateinit var binding: LauncherActivityBinding
@ -405,14 +498,20 @@ open class LauncherActivity : CommonActivity() {
}
// 두 뷰가 겹치는지 확인하는 헬퍼 함수
private fun isViewOverlapping(v1: View, v2: View): Boolean {
val rect1 = android.graphics.Rect()
v1.getGlobalVisibleRect(rect1)
private fun isViewOverlapping(dragView: View, targetView: View): Boolean {
val targetRect = android.graphics.Rect()
targetView.getGlobalVisibleRect(targetRect)
val rect2 = android.graphics.Rect()
v2.getGlobalVisibleRect(rect2)
// 2. 드래그 중인 뷰의 화면상 절대 좌표 영역 구하기
val dragRect = android.graphics.Rect()
dragView.getGlobalVisibleRect(dragRect)
return android.graphics.Rect.intersects(rect1, rect2)
// 3. 드래그 뷰의 중심점 계산
val centerX = dragRect.centerX()
val centerY = dragRect.centerY()
// 4. 중심점이 타겟 영역 안에 있는지 확인
return targetRect.contains(centerX, centerY)
}
// 위젯 관련 변수
private var appWidgetManager: AppWidgetManager? = null
@ -421,6 +520,13 @@ open class LauncherActivity : CommonActivity() {
private val REQUEST_PICK_APPWIDGET = 100
private val REQUEST_CREATE_APPWIDGET = 101
fun showAppDrawer() {
val bottomSheet = AppDrawerBottomSheet.newInstance()
bottomSheet.show(supportFragmentManager, AppDrawerBottomSheet.TAG)
}
private lateinit var homeGestureDetector: androidx.core.view.GestureDetectorCompat
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@ -446,6 +552,18 @@ open class LauncherActivity : CommonActivity() {
DynamicColors.applyToActivityIfAvailable(this)
binding = LauncherActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
homeGestureDetector = androidx.core.view.GestureDetectorCompat(this, HomeGestureListener())
binding.widgetContainer.setOnTouchListener { view, event ->
// 위젯 드래그 중이 아닐 때만 제스처 처리
if (!isDraggingWidget) {
homeGestureDetector.onTouchEvent(event)
// true를 반환해야 이벤트가 소비되어 onSingleTap 등이 정상 동작함
// 하지만 자식 뷰(위젯)의 클릭을 막지 않으려면 주의 필요.
// onTouchEvent가 true를 반환하면 이벤트 체인이 여기서 끝납니다.
return@setOnTouchListener true
}
false
}
HeadsetActionButtonReceiver.register(this)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
@ -789,6 +907,15 @@ open class LauncherActivity : CommonActivity() {
R.id.setting ->{
startActivity(Intent(this, SettingsActivity::class.java))
}
R.id.close ->{
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
supportFragmentManager.beginTransaction()
.remove(it)
.commit()
binding.fragmentContainer.visibility = View.GONE
}
}
else -> {}
}
binding.floatingActionMenu.close(false)

View File

@ -45,6 +45,7 @@ import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.helpers.PrefLong
import bums.lunatic.launcher.helpers.PrefString
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.WorkersDb

View File

@ -0,0 +1,338 @@
package bums.lunatic.launcher.apps
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.R
import bums.lunatic.launcher.databinding.BottomSheetAppDrawerBinding // XML 이름에 맞춰 바인딩 클래스 생성됨
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.helpers.PrefLong
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.WorkersDb
import com.google.android.gms.common.wrappers.PackageManagerWrapper
import com.google.android.gms.common.wrappers.Wrappers.packageManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.realm.kotlin.ext.query
import io.realm.kotlin.query.Sort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppDrawerBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetAppDrawerBinding? = null
private val binding get() = _binding!!
private var appsAdapter: AppsAdapter? = null
private var contactAdapter: ContactAdapter? = null
private val packageList = mutableListOf<AppInfo>()
private val contactList = arrayListOf<SimpleContact>() // SimpleContact 클래스가 import 되어야 함
companion object {
const val TAG = "AppDrawerBottomSheet"
fun newInstance(): AppDrawerBottomSheet {
return AppDrawerBottomSheet()
}
}
// 배경을 투명하게 하거나 스타일을 적용하기 위해
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// res/values/themes.xml 등에 BottomSheet 스타일 정의 필요 (모서리 둥글게 등)
setStyle(STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme)
}
// 키보드가 올라올 때 바텀시트가 가려지지 않도록 설정
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
// 바텀시트가 완전히 펼쳐진 상태로 시작하게 설정
dialog.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = BottomSheetAppDrawerBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAdapters()
setupSearchButtons()
setupListeners()
// 초기 데이터 로드
fetchApps()
}
override fun onStart() {
super.onStart()
val dialog = dialog as? com.google.android.material.bottomsheet.BottomSheetDialog
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.let { bottomSheet ->
// 1. 동작 설정 (완전히 펼치기)
val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bottomSheet)
behavior.state = com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
// 2. [핵심] 높이 강제 설정 (이게 없으면 리스트가 0dp일 때 안 보임)
val layoutParams = bottomSheet.layoutParams
layoutParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT
bottomSheet.layoutParams = layoutParams
}
}
private var recAdapter: AppsAdapter? = null
private fun setupAdapters() {
// 기존 Activity의 packageManager 대신 requireContext().packageManager 사용
val pm = requireContext().packageManager
appsAdapter = AppsAdapter(pm, childFragmentManager, binding.appsCount)
contactAdapter = ContactAdapter(pm, childFragmentManager)
// 가로 그리드 개수 4~5개 정도로 조정 (기존 2개였으면 그대로 유지)
binding.appsList.layoutManager = GridLayoutManager(context, 3,GridLayoutManager.HORIZONTAL,false)
binding.appsList.adapter = appsAdapter
binding.appsList.setItemViewCacheSize(20)
binding.appsList.setHasFixedSize(true) //
binding.contactList.layoutManager = GridLayoutManager(context, 3,GridLayoutManager.HORIZONTAL,false)
binding.contactList.adapter = contactAdapter
recAdapter = AppsAdapter(pm, childFragmentManager,null)
binding.recAppsList.layoutManager = GridLayoutManager(context, 1,GridLayoutManager.HORIZONTAL,false)
binding.recAppsList.adapter = recAdapter
}
private fun setupListeners() {
// 검색어 입력 리스너
binding.searchInput.doOnTextChanged { inputText, _, _, _ ->
filterAppsList(inputText.toString())
}
// 엔터키 입력 시 검색 실행
binding.searchInput.setOnKeyListener { _, keyCode, event ->
if (PrefBoolean.useQuickLaunch.get(false) && keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP) {
checkResult(binding.searchInput.text.toString())
true
} else {
false
}
}
// 새로고침 버튼
binding.reset.setOnClickListener {
binding.searchInput.text?.clear()
fetchApps()
}
}
private fun setupSearchButtons() {
// 기존 AppDrawer의 버튼 연결
binding.searchNmap.setOnClickListener {
openSearchApps("nmap://search?query=${getInputText()}&appname=${BuildConfig.APPLICATION_ID}", "com.nhn.android.nmap")
}
binding.searchYoutube.setOnClickListener {
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")
}
binding.searchNaver.setOnClickListener {
openSearchApps("https://search.naver.com/search.naver?where=nexearch&query=${getInputText()}", "com.nhn.android.search")
}
// ... 나머지 버튼들도 동일하게 추가 ...
}
private fun getInputText() = binding.searchInput.text.toString()
private fun openSearchApps(schemeString: String, packageName: String? = null) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(schemeString))
packageName?.let {
intent.setPackage(it)
WorkersDb.updateAppUse(it)
}
startActivity(intent)
dismiss() // 실행 후 닫기
} catch (e: Exception) {
e.printStackTrace()
// 앱이 없을 경우 웹으로 열기 등의 예외처리 권장
}
}
private fun checkResult(keyword: String) {
// 기존 Activity의 checkResult 로직 구현 (필요시 부모 액티비티 함수 호출)
// (lActivity as? LauncherActivity)?.openSearchMenus(keyword) ...
dismiss()
}
private fun filterAppsList(searchString: String) {
fetchApps(searchString)
}
/**
* 목록을 불러오는 함수
* - 검색어(keyword) 있으면 필터링을 수행합니다.
* - 검색어가 없으면 '문맥 기반 추천 ' 최상단에 배치합니다.
* - IO 스레드에서 실행하여 UI 끊김을 방지합니다.
*/
private fun fetchApps(keyword: String? = null) {
lifecycleScope.launch(Dispatchers.IO) {
val realm = WorkersDb.getRealm()
val pm = requireContext().packageManager
// 1. [추천 로직] 에러가 나도 앱 목록 로딩은 진행되도록 try-catch 분리
val recommendedPkgNames = try {
if (keyword.isNullOrEmpty()) {
WorkersDb.getContextualRecommendations(limit = 5)
} else {
emptyList() // 검색 중에는 추천 안 함
}
} catch (e: Exception) { emptyList() }
try {
// 2. [쿼리 구성]
var appQuery = realm.query<AppInfo>()
if (!keyword.isNullOrEmpty()) {
val firstChar = keyword.first().toString()
if (JamoUtils.CHOSUNG.contains(firstChar)) {
appQuery = appQuery.query("appNameChosung CONTAINS[c] $0 OR alphaCho CONTAINS[c] $0", keyword)
} else if (java.util.regex.Pattern.matches("^[가-힣]*\$", keyword)) {
appQuery = appQuery.query("appName CONTAINS[c] $0 OR koreanName CONTAINS[c] $0", keyword)
} else {
appQuery = appQuery.query("appName CONTAINS[c] $0 OR pkgName CONTAINS[c] $0 OR category CONTAINS[c] $0", keyword)
}
}
// 3. [DB 조회]
val results = appQuery
.sort("clickCount", Sort.DESCENDING)
.sort("lastUseDate", Sort.DESCENDING)
.find()
// [중요] DB가 비어있다면(앱 설치 직후 등), 여기서 앱 스캔을 요청하거나 빈 상태 처리
if (results.isEmpty() && keyword.isNullOrEmpty()) {
// 필요 시 AppInfoGetter 워커를 즉시 실행하는 로직 추가 가능
}
// 4. [데이터 가공]
val allApps = results.map { realm.copyFromRealm(it) }
.filter { appInfo ->
try {
pm.getLaunchIntentForPackage(appInfo.pkgName ?: "") != null
} catch (e: Exception) {
false
}
}
// C. [추천 앱 객체 추출]
val recAppList = recommendedPkgNames.mapNotNull { pkg ->
allApps.find { it.pkgName == pkg }
}
val mainAppList = allApps
var contactQuery = realm.query<SimpleContact>()
if (!keyword.isNullOrEmpty()) {
// 이름, 초성, 전화번호로 검색
val firstChar = keyword.first().toString()
if (JamoUtils.CHOSUNG.contains(firstChar)) {
contactQuery = contactQuery.query("chosung CONTAINS[c] $0", keyword)
} else if (java.util.regex.Pattern.matches("^[가-힣]*\$", keyword)) {
contactQuery = contactQuery.query("name CONTAINS[c] $0", keyword)
} else {
contactQuery = contactQuery.query("name CONTAINS[c] $0 OR phoneNumber CONTAINS $0", keyword)
}
} else {
// 검색어 없을 때: 자주 쓰는 연락처(터치 횟수 순) 또는 최근 연락처 상위 10개만 노출 (너무 많으면 스크롤 힘듦)
}
contactQuery = contactQuery.sort("touchCount", Sort.DESCENDING).limit(10)
val contactsResult = contactQuery.find()
val contactsList = contactsResult.map { realm.copyFromRealm(it) }
// 6. [UI 업데이트]
withContext(Dispatchers.Main) {
if (recAppList.isNotEmpty() && keyword.isNullOrEmpty()) {
binding.titleRecommend.visibility = View.VISIBLE
binding.recAppsList.visibility = View.VISIBLE
recAdapter?.updateData(recAppList)
} else {
// 검색 중이거나 데이터 없으면 숨김
binding.titleRecommend.visibility = View.GONE
binding.recAppsList.visibility = View.GONE
}
// 2. 전체 앱 리스트 업데이트
packageList.clear()
packageList.addAll(mainAppList)
appsAdapter?.updateData(packageList)
binding.appsCount.text = "${packageList.size} Apps"
if (contactsList.isNotEmpty()) {
binding.titleContact.visibility = View.VISIBLE
binding.contactList.visibility = View.VISIBLE
contactAdapter?.updateData(contactsList)
} else {
// 검색 결과가 없으면 숨김
binding.titleContact.visibility = View.GONE
binding.contactList.visibility = View.GONE
contactAdapter?.updateData(emptyList()) // 빈 리스트로 갱신하여 잔상 제거
}
}
} catch (e: Exception) {
e.printStackTrace() // 여기서 에러가 나면 앱 목록이 안 뜹니다. 로그캣(Logcat)을 확인해보세요.
}
}
}
private fun isPackageInstalled(packageName: String, packageManager: PackageManagerWrapper): Boolean {
return try {
packageManager.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -38,6 +38,7 @@ import bums.lunatic.launcher.workers.WorkersDb
import io.realm.kotlin.ext.query
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
internal class AppsAdapter(
@ -64,11 +65,7 @@ internal class AppsAdapter(
holder.view.apply {
childTextview.text = item.appName
appIconTwo.visibility = View.VISIBLE
MainScope().async {
getDrawableIconForPackage(item.pkgName, packageManager.getApplicationIcon(item.pkgName!!)) {
appIconTwo.post { appIconTwo.setImageDrawable(it) }
} }
loadIconAsync(appIconTwo, item.pkgName)
childTextview.apply {
gravity = Gravity.CENTER
setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twelve))
@ -90,6 +87,7 @@ internal class AppsAdapter(
}
}
}
item.pkgName?.let { WorkersDb.logAppUsage(it,"APP") }
context.startActivity(packageManager.getLaunchIntentForPackage(item.pkgName!!))
}
@ -112,6 +110,28 @@ internal class AppsAdapter(
}
}
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)
@ -119,10 +139,17 @@ internal class AppsAdapter(
/* update app list */
fun updateData(newList: List<AppInfo>) {
val diffUtilResult = DiffUtil.calculateDiff(AppsDiffUtil(oldList, newList))
//
diffUtilResult.dispatchUpdatesTo(this)
// [수정 전] dispatchUpdatesTo가 먼저 있어서 에러 발생함
// diffUtilResult.dispatchUpdatesTo(this)
// oldList.clear()
// oldList.addAll(newList)
// [수정 후] 반드시 리스트 데이터를 먼저 갱신하고 나서 알림을 보내야 합니다!
oldList.clear()
oldList.addAll(newList)
diffUtilResult.dispatchUpdatesTo(this) // <-- 순서 변경 (맨 뒤로)
newList.size.let {
appsCount?.text = it.toString()
appsSize = it

View File

@ -27,6 +27,7 @@ import androidx.fragment.app.FragmentManager
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
@ -80,9 +81,9 @@ internal class ContactAdapter (
synchronized(oldList) {
try {
val diffUtilResult = DiffUtil.calculateDiff(ContactDiffUtil(oldList, newList))
diffUtilResult.dispatchUpdatesTo(this)
oldList.clear()
oldList.addAll(newList)
diffUtilResult.dispatchUpdatesTo(this)
newList.size.let {
appsSize = it
}
@ -107,25 +108,7 @@ internal class ContactAdapter (
}
}
class SimpleContact : RealmObject {
@PrimaryKey
var id : String? = ""
var name : String? = ""
var chosung : String? = ""
var phoneNumber : String? = ""
var touchCount = 0
var lastedTouchDateTime = 0L
constructor(id: String, name: String, phoneNumber: String) {
this.id = id
this.name = name
this.phoneNumber = phoneNumber
chosung = JamoUtils.split(name).joinToString("")
}
constructor()
}
internal class ContactDiffUtil(
private val oldList: List<SimpleContact>, private val newList: List<SimpleContact>

View File

@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup
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.WorkersDb
import com.google.android.material.bottomsheet.BottomSheetDialog

View File

@ -1,243 +1,43 @@
/*
* Lunar Launcher
* Copyright (C) 2022 Md Rasel Hossain
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package bums.lunatic.launcher.apps
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.collection.LruCache
import androidx.core.content.res.ResourcesCompat
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.helpers.Constants.Companion.DEFAULT_ICON_PACK
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_ICON_PACK
import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_PKGICS
import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.ImageUtils
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.util.Locale
import android.util.LruCache // 이거 임포트
internal class IconPackManager {
@SuppressLint("DiscouragedApi")
class IconPackManager {
companion object {
private val settingsPrefs = lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0)
private val icsPrefs = lActivity!!.getSharedPreferences(PREFS_PKGICS,0)
private val packageName = settingsPrefs.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK)
private var loaded = false
private val packagesDrawables = HashMap<String?, String?>()
private val packagesConponentNames = HashMap<String?, String?>()
private val backImages: MutableList<Bitmap> = ArrayList()
private var maskImage: Bitmap? = null
private var frontImage: Bitmap? = null
private var factor = 1.0f
private var totalIcons = 0
private var iconPackRes: Resources? = null
private var appPackageIconDrawables : HashMap<String, Drawable> = hashMapOf()
private fun load() {
/* load appfilter.xml from the icon pack package */
try {
var xpp: XmlPullParser? = null
iconPackRes = lActivity!!.packageManager.getResourcesForApplication(packageName!!)
val appFilterId = iconPackRes!!.getIdentifier("appfilter", "xml", packageName)
if (appFilterId > 0) {
xpp = iconPackRes!!.getXml(appFilterId)
// BLog.LOGE("packageName >>> ${packageName}")
} else {
try {
xpp = XmlPullParserFactory.newInstance().apply { isNamespaceAware = true }
.newPullParser().apply {
// BLog.LOGE("packageName >>> ${packageName}")
setInput(iconPackRes!!.assets.open("appfilter.xml"), "utf-8")
}
} catch (e: IOException) {
e.printStackTrace()
Blog.w("", "Couldn't find the appfilter.xml file")
}
}
if (xpp != null) {
var eventType = xpp.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
when (xpp.name) {
"iconback" -> {
for (i in 0 until xpp.attributeCount) { if (xpp.getAttributeName(i).startsWith("img")) { loadBitmap(xpp.getAttributeValue(i))?.let { backImages.add(it) }}}
}
"iconmask" -> {
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") { maskImage = loadBitmap(xpp.getAttributeValue(0)) }
}
"iconupon" -> {
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") { frontImage = loadBitmap(xpp.getAttributeValue(0)) }
}
"scale" -> {
if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "factor") { factor = java.lang.Float.valueOf(xpp.getAttributeValue(0)) }
}
"item" -> {
var componentName: String? = null
var drawableName: String? = null
for (i in 0 until xpp.attributeCount) { when (xpp.getAttributeName(i)) {
"component" -> componentName = xpp.getAttributeValue(i)
"drawable" -> drawableName = xpp.getAttributeValue(i)
} }
if (!packagesDrawables.containsKey(componentName)) {
packagesDrawables[componentName] = drawableName
totalIcons += 1
}
}
}
}
eventType = xpp.next()
}
}
loaded = true
} catch (e: PackageManager.NameNotFoundException) {
Blog.w("", "Failed to load the icon pack")
} catch (e: XmlPullParserException) {
Blog.w("", "Failed to parse the appfilter.xml file")
} catch (e: IOException) {
e.printStackTrace()
}
}
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
val bitmapCache = object : LruCache<String, Bitmap>(cacheSize) {
fun sizeOf(key: String?, value: Bitmap?): Int {
return if(value?.byteCount?.toInt() ?: 0 > 1024) {
value?.byteCount!!.div(1024)
} else { 0 }
}
// [추가] 메모리 캐시 (최대 4MB 또는 아이콘 100개 분량 등 설정)
private val iconCache = object : LruCache<String, Drawable>(100) {
// 사이즈 측정 로직이 필요하면 sizeOf 오버라이드 (여기선 갯수 기준 100개)
}
private fun loadBitmap(drawableName: String): Bitmap? {
if (packageName != null && packageName.length > 0) {
var bm = bitmapCache.get(packageName)
GlobalScope.async {
bm?.let { ImageUtils.bitmapToBase64String(it)?.let {
icsPrefs.contains(packageName)
} }
}
if (bm != null) return bm
}
iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let { id ->
if (id > 0) {
ResourcesCompat.getDrawable(iconPackRes!!, id, null).let {
if (it is BitmapDrawable) {
if (packageName != null && packageName.length > 0) {
bitmapCache.put(packageName, it.bitmap)
}
return it.bitmap
}
}
}
}
return null
fun getDrawableIconForPackage(context: Context, packageName: String): Drawable? {
// 1. 캐시에 있는지 확인
val cachedIcon = iconCache.get(packageName)
if (cachedIcon != null) {
return cachedIcon
}
private fun loadDrawable(drawableName: String): Drawable? {
iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let {
return if (it > 0) ResourcesCompat.getDrawable(iconPackRes!!, it, null)
else null
}
// 2. 없으면 로딩 (기존 로직 수행)
// ... (기존의 아이콘팩 로딩 또는 pm.getApplicationIcon 코드) ...
val loadedIcon = try {
context.packageManager.getApplicationIcon(packageName)
// 만약 아이콘팩 적용 로직이 있다면 여기서 변환 수행
} catch (e: Exception) {
context.resources.getDrawable(android.R.drawable.sym_def_app_icon, null)
}
fun putAfterReturn(packages: String, drawable : Drawable?) : Drawable? {
if (drawable != null) {
appPackageIconDrawables.put(packages, drawable)
(drawable as? BitmapDrawable)?.let {
if(icsPrefs.contains(packageName)) {
} else {
icsPrefs.edit().putString(packageName, ImageUtils.bitmapToBase64String(it.bitmap)).apply()
}
}
}
return drawable
}
fun getDrawableIconForPackage(appPackageName: String?, defaultDrawable: Drawable?, onComplete : (Drawable?)->Unit) {
var ddd = if (appPackageIconDrawables.containsKey(appPackageName)) appPackageIconDrawables.get(appPackageName) else null
if (ddd != null) {
onComplete(ddd)
} else {
when (packageName) {
DEFAULT_ICON_PACK -> onComplete.invoke(defaultDrawable)
else -> {
if (!loaded) load()
var componentName: String? = null
componentName = packagesConponentNames.get(appPackageName!!)
if (componentName == null || componentName.length ?: 0 <= 0 ) {
Blog.LOGE("it's compo ${appPackageName}")
var pkgIntent =
lActivity!!.packageManager.getLaunchIntentForPackage(
appPackageName!!
)
if (pkgIntent != null) {
componentName = pkgIntent!!.component.toString()
}
}
var drawable = packagesDrawables[componentName]
if (!drawable.isNullOrEmpty()) onComplete.invoke(
putAfterReturn(
appPackageName,
loadDrawable(drawable)
)
)
else {
if (!componentName.isNullOrEmpty()) {
val start = componentName.indexOf("{") + 1
val end = componentName.indexOf("}", start)
if (end > start) {
drawable = componentName.substring(start, end)
.lowercase(Locale.getDefault()).replace(".", "_")
.replace("/", "_")
try {
if (iconPackRes!!.getIdentifier(
drawable,
"drawable",
packageName
) > 0
) onComplete.invoke(putAfterReturn(appPackageName, loadDrawable(drawable)))
} catch (e: NullPointerException) {
settingsPrefs.edit()
.putString(KEY_ICON_PACK, DEFAULT_ICON_PACK).apply()
}
}
} else {
onComplete.invoke(defaultDrawable)
}
}
}
}
// 3. 로딩된 아이콘 캐시에 저장
if (loadedIcon != null) {
iconCache.put(packageName, loadedIcon)
}
return loadedIcon
}
// 아이콘팩 변경 시 호출해줄 함수
fun clearCache() {
iconCache.evictAll()
}
}
}

View File

@ -0,0 +1,68 @@
package bums.lunatic.launcher.apps
import android.content.pm.PackageManager
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import bums.lunatic.launcher.databinding.AppsChildBinding // 기존 레이아웃 재사용 (또는 별도 레이아웃 생성)
import bums.lunatic.launcher.databinding.AppsChildRecBinding
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
class RecommendedAppsAdapter(private val pm: PackageManager) : RecyclerView.Adapter<RecommendedAppsAdapter.ViewHolder>() {
private val items = ArrayList<AppInfo>()
fun submitList(newItems: List<AppInfo>) {
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)
}
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)
}
}
// 클릭 이벤트
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 getItemCount(): Int = items.size
class ViewHolder(val binding: AppsChildRecBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -20,29 +20,19 @@ import android.os.IBinder
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import bums.lunatic.launcher.LauncherActivity
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.R
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.ArcaGetter
import bums.lunatic.launcher.workers.ClienGetter
import bums.lunatic.launcher.workers.DCGetter
import bums.lunatic.launcher.workers.DotaxGetter
import bums.lunatic.launcher.workers.DotaxGetter.Companion.COMIC2_WORK_TAG
import bums.lunatic.launcher.workers.FmKoreaGetter
import bums.lunatic.launcher.workers.FmKoreaGetter.Companion.FM_WORK_TAG
import bums.lunatic.launcher.workers.LocationGetter
import bums.lunatic.launcher.workers.NewsFeedsGetter
import bums.lunatic.launcher.workers.NewsFeedsGetter.Companion.FEDDS_WORK_TAG
import bums.lunatic.launcher.workers.RedditGetter
import bums.lunatic.launcher.workers.RedditGetter.Companion.REDDIT_WORK_TAG
import bums.lunatic.launcher.workers.RuliWebGetter
import bums.lunatic.launcher.workers.TheQooGetter
import bums.lunatic.launcher.workers.YoutubeGetter
import bums.lunatic.launcher.workers.YoutubeGetter.Companion.YT_WORK_TAG
import bums.lunatic.launcher.workers.TaskAggregator
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
@ -58,6 +48,22 @@ import java.util.UUID
import java.util.concurrent.TimeUnit
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 통합 시스템 작업 실행
TaskAggregator.refreshSystemInfo(context)
return Result.success()
}
}
class AggregatedNewsWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 통합 뉴스 작업 실행
TaskAggregator.refreshNewsFeeds(context = context)
return Result.success()
}
}
class ForeGroundService : Service() {
companion object {
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
@ -225,85 +231,26 @@ class ForeGroundService : Service() {
}
fun refreshFeeds() {
mWorkManager?.cancelAllWork()
mWorkManager?.cancelAllWorkByTag(RuliWebGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
RuliWebGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<RuliWebGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(RuliWebGetter.TAG)
.build())
val workManager = WorkManager.getInstance(applicationContext)
mWorkManager?.cancelAllWorkByTag(FEDDS_WORK_TAG)
mWorkManager?.enqueueUniquePeriodicWork(
FEDDS_WORK_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<NewsFeedsGetter>(PrefLong.shortTimePeriod.get(), TimeUnit.MINUTES)
.addTag(FEDDS_WORK_TAG)
.build())
mWorkManager?.cancelAllWorkByTag(YT_WORK_TAG)
mWorkManager?.enqueueUniquePeriodicWork(
YT_WORK_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<YoutubeGetter>(PrefLong.longTimePeriod.get(), TimeUnit.MINUTES)
.addTag(YT_WORK_TAG)
.build())
mWorkManager?.cancelAllWorkByTag(REDDIT_WORK_TAG)
mWorkManager?.enqueueUniquePeriodicWork(
REDDIT_WORK_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<RedditGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(REDDIT_WORK_TAG)
.build())
mWorkManager?.cancelAllWorkByTag(FM_WORK_TAG)
mWorkManager?.enqueueUniquePeriodicWork(
FM_WORK_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<FmKoreaGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(FM_WORK_TAG)
.build())
mWorkManager?.cancelAllWorkByTag(COMIC2_WORK_TAG)
mWorkManager?.enqueueUniquePeriodicWork(
COMIC2_WORK_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<DotaxGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(COMIC2_WORK_TAG)
.build())
mWorkManager?.cancelAllWorkByTag(ClienGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
ClienGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<ClienGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(ClienGetter.TAG)
.build())
mWorkManager?.cancelAllWorkByTag(DCGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
DCGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<DCGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(DCGetter.TAG)
.build())
mWorkManager?.cancelAllWorkByTag(TheQooGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
TheQooGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<TheQooGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(TheQooGetter.TAG)
.build())
mWorkManager?.cancelAllWorkByTag(ArcaGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
ArcaGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<ArcaGetter>(PrefLong.midTimePeriod.get(), TimeUnit.MINUTES)
.addTag(ArcaGetter.TAG)
.build())
mWorkManager?.cancelAllWorkByTag(LocationGetter.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
LocationGetter.TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<LocationGetter>(PrefLong.locationTimePeriod.get(), TimeUnit.MINUTES)
.addTag(LocationGetter.TAG)
.build())
mWorkManager?.cancelAllWorkByTag(ServiceWatchdogWorker.TAG)
mWorkManager?.enqueueUniquePeriodicWork(
ServiceWatchdogWorker.TAG,
ExistingPeriodicWorkPolicy.REPLACE,
PeriodicWorkRequestBuilder<ServiceWatchdogWorker>(15, TimeUnit.MINUTES)
.addTag(ServiceWatchdogWorker.TAG)
.build())
TaskAggregator.refreshSystemInfo(applicationContext)
// 1. 시스템 정보: 자주 변경되지 않으므로 1~3시간 간격
val systemRequest = PeriodicWorkRequestBuilder<AggregatedSystemWorker>(PrefLong.longTimePeriod.get(120L), TimeUnit.MINUTES)
.build()
// 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간)
val newsRequest = PeriodicWorkRequestBuilder<AggregatedNewsWorker>(PrefLong.shortTimePeriod.get(20L), TimeUnit.MINUTES)
.build()
// 기존의 수많은 enqueue 코드를 이 두 개로 대체
workManager.enqueueUniquePeriodicWork("AggregatedSystemWork", ExistingPeriodicWorkPolicy.KEEP, systemRequest)
workManager.enqueueUniquePeriodicWork("AggregatedNewsWork", ExistingPeriodicWorkPolicy.KEEP, newsRequest)
}
fun workmanager() : WorkManager? {
if (mWorkManager == null && lActivity != null) {
mWorkManager = WorkManager.getInstance(lActivity!!)

View File

@ -18,4 +18,9 @@ class AppInfo : RealmObject {
var category : String? = null
var currentInstalled : Boolean = false
var isInstalled : Boolean = false
// [신규] 0: 항상 보이기(기본), 1: 검색 시만 보이기 (숨김)
var visibilityMode: Int = 0
// [신규] true면 추천 리스트에 절대 안 뜸
var blockRecommend: Boolean = false
}

View File

@ -0,0 +1,19 @@
package bums.lunatic.launcher.model
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import org.mongodb.kbson.ObjectId
class AppUsageLog : RealmObject {
@PrimaryKey
var _id: ObjectId = ObjectId()
var itemKey: String = "" // 패키지명(앱) 또는 연락처 URI(연락처)
var itemType: String = "APP" // "APP" 또는 "CONTACT"
var timestamp: Long = 0L // 사용 시간 (밀리초)
var month: Int = 0 // 0~11 (Calendar.MONTH)
var dayOfMonth: Int = 0 // 1~31
var dayOfWeek: Int = 0 // 1(일)~7(토)
var hour: Int = 0 // 0~23 (24시간제)
}

View File

@ -0,0 +1,29 @@
package bums.lunatic.launcher.model
import bums.lunatic.launcher.utils.JamoUtils
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
class SimpleContact : RealmObject {
@PrimaryKey
var id : String? = ""
var name : String? = ""
var chosung : String? = ""
var phoneNumber : String? = ""
var touchCount = 0
var lastedTouchDateTime = 0L
// [신규] 0: 항상 보이기(기본), 1: 검색 시만 보이기 (숨김)
var visibilityMode: Int = 0
// [신규] true면 추천 리스트에 절대 안 뜸
var blockRecommend: Boolean = false
constructor(id: String, name: String, phoneNumber: String) {
this.id = id
this.name = name
this.phoneNumber = phoneNumber
chosung = JamoUtils.split(name).joinToString("")
}
constructor()
}

View File

@ -45,7 +45,6 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.R
import bums.lunatic.launcher.apps.SimpleContact
import bums.lunatic.launcher.databinding.QuickAccessBinding
import bums.lunatic.launcher.databinding.ShortcutMakerBinding
import bums.lunatic.launcher.helpers.ColorPicker
@ -60,6 +59,7 @@ import bums.lunatic.launcher.helpers.Constants.Companion.SEPARATOR
import bums.lunatic.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_PHONE
import bums.lunatic.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_URL
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.material.bottomsheet.BottomSheetDialog

View File

@ -43,9 +43,9 @@ class PackageEventReceiver : BroadcastReceiver() {
}
fun startAppInfoGetter(context: Context) {
var mWorkManager = WorkManager.getInstance(context)
Executors.newSingleThreadScheduledExecutor().schedule({
mWorkManager.enqueue(OneTimeWorkRequest.from(AppInfoGetter::class.java))
}, 5, TimeUnit.SECONDS)
// var mWorkManager = WorkManager.getInstance(context)
// Executors.newSingleThreadScheduledExecutor().schedule({
// mWorkManager.enqueue(OneTimeWorkRequest.from(AppInfoGetter::class.java))
// }, 5, TimeUnit.SECONDS)
}
}

View File

@ -25,7 +25,6 @@ import android.os.Bundle
import android.os.Environment
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.R
import bums.lunatic.launcher.apps.SimpleContact
import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.AboutBinding
import bums.lunatic.launcher.databinding.SettingsActivityBinding
@ -34,6 +33,7 @@ import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.helpers.PrefHelper
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.settings.childs.Apps
import bums.lunatic.launcher.settings.childs.HomeSettings
import bums.lunatic.launcher.settings.childs.Misc

View File

@ -12,8 +12,10 @@ import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.apps.AppDrawer.Companion.appNamesPrefs
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.utils.AlphabetToChosungMap
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.JamoUtils
import io.realm.kotlin.ext.query
import io.realm.kotlin.types.RealmObject
import java.text.Normalizer
import java.util.regex.Pattern
@ -21,48 +23,49 @@ class AppInfoGetter : BaseGetter {
companion object {
val TAG = "AppInfoGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
constructor(context: Context) : super(context) {
}
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
var result = mutableListOf<RealmObject>()
try {
val packageManager = lActivity?.packageManager ?: return result
var packageManager = lActivity?.packageManager
var packageInfoList: MutableList<ResolveInfo> = mutableListOf()
packageInfoList = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager?.queryIntentActivities(
Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER),
PackageManager.ResolveInfoFlags.of(0)
)
// 1. 설치된 앱 목록 가져오기 (시스템 호출 1회)
val intent = Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER)
val resolveInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))
} else {
(packageManager?.queryIntentActivities(
Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER), 0
))
})?.apply {
removeIf { it.activityInfo.packageName.equals(BuildConfig.APPLICATION_ID) }
forEach {
val result = WorkersDb.getRealm().query<AppInfo>().query("pkgName == $0",it.activityInfo.packageName).find()
if (result.size < 1) {
val info = AppInfo()
info.appName = normalize(appName(it))
info.pkgName = it.activityInfo.packageName
info.category = getCategory(it.activityInfo.applicationInfo.category)
info.alphaCho = AlphabetToChosungMap.getCho(info.appName!!)
info.appNameChosung = JamoUtils.split(info.appName).joinToString("")
WorkersDb.update(info)
} else{
val info = WorkersDb.getRealm().copyFromRealm(result.first())
info.alphaCho = AlphabetToChosungMap.getCho(info.appName!!)
info.appNameChosung = JamoUtils.split(info.appName).joinToString("")
WorkersDb.update(info)
packageManager.queryIntentActivities(intent, 0)
}
}
}!!
} catch (e : Exception) {e.printStackTrace()}
return Result.success()
resolveInfos.forEach { ri ->
val pkgName = ri.activityInfo.packageName
if (pkgName == BuildConfig.APPLICATION_ID) return@forEach
// 이미 DB에 있는지 Map에서 확인 (고속 검색)
// 신규 앱 발견 -> 추가
val appName = normalize(appName(ri))
result.add(AppInfo().apply {
this.appName = appName
this.pkgName = pkgName
this.category = getCategory(ri.activityInfo.applicationInfo.category)
this.alphaCho = AlphabetToChosungMap.getCho(appName)
this.appNameChosung = JamoUtils.split(appName).joinToString("")
})
}
} catch (e: Exception) {
e.printStackTrace()
return result
}
return result
}
fun appName(resolver: ResolveInfo): String {
return resolver.loadLabel(lActivity?.packageManager!!).toString().apply {
appNamesPrefs?.edit()?.putString(resolver.activityInfo.packageName, this)?.apply()

View File

@ -1,108 +1,108 @@
package bums.lunatic.launcher.workers
//import bums.lunatic.launcher.workers.WorkersDb.blockKeyword
import android.content.Context
import androidx.work.WorkerParameters
import bums.lunatic.launcher.model.Arca
import bums.lunatic.launcher.model.RssDataInterface
import bums.lunatic.launcher.model.getT
import bums.lunatic.launcher.utils.beforeOneDay
import org.jsoup.nodes.Element
class ArcaGetter : BaseGetter {
companion object {
val TAG = "ArcaGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
override fun realWork(): Result {
// RssDataType.ARCA.isOn {
// try {
// Blog.LOGE("realWork() ${this::class.simpleName}")
// temp.clear()
// val urls = arrayListOf(
// "https://arca.live/b/singbung?mode=best",
//// "https://arca.live/b/headline",
//// "https://arca.live/b/live",
// "https://arca.live/b/namuhotnow",
// "https://arca.live/b/society",
//// "https://arca.live/b/replay",
//// "https://arca.live/b/breaking"
// )
// urls.forEach {
//package bums.lunatic.launcher.workers
//
////import bums.lunatic.launcher.workers.WorkersDb.blockKeyword
//import android.content.Context
//import androidx.work.WorkerParameters
//import bums.lunatic.launcher.model.Arca
//import bums.lunatic.launcher.model.RssDataInterface
//import bums.lunatic.launcher.model.getT
//import bums.lunatic.launcher.utils.beforeOneDay
//import org.jsoup.nodes.Element
//
//class ArcaGetter : BaseGetter {
// companion object {
// val TAG = "ArcaGetter"
// }
// constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
//
// }
//
// override fun realWork(): Result {
//// RssDataType.ARCA.isOn {
//// try {
//// Jsoup.connect(it)
//// .userAgent(USAGT)
//// .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
//// .header("accept-language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
//// .header("cache-control", "no-cache")
//// .header("pragma", "no-cache")
//// .ignoreContentType(true)
//// .timeout(5000)
//// .get().let { arca ->
////// BLog.LOGE("url >> ${it} >> ${arca}")
//// arca.getElementsByClass("vrow hybrid").forEach { araca_li ->
//// if (araca_li.html().contains("title ") == true) {
//// parseArcaLi(araca_li).apply {
//// this.forEach {
//// if (it.pubDate() > commicsDateTime) {
//// temp.add(it.getRssData())
//// }
//// }
//// }
//// }
//// }
//// Blog.LOGE("realWork() ${this::class.simpleName}")
//// temp.clear()
//// val urls = arrayListOf(
//// "https://arca.live/b/singbung?mode=best",
////// "https://arca.live/b/headline",
////// "https://arca.live/b/live",
//// "https://arca.live/b/namuhotnow",
//// "https://arca.live/b/society",
////// "https://arca.live/b/replay",
////// "https://arca.live/b/breaking"
//// )
//// urls.forEach {
////// try {
////// Jsoup.connect(it)
////// .userAgent(USAGT)
////// .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
////// .header("accept-language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
////// .header("cache-control", "no-cache")
////// .header("pragma", "no-cache")
////// .ignoreContentType(true)
////// .timeout(5000)
////// .get().let { arca ->
//////// BLog.LOGE("url >> ${it} >> ${arca}")
////// arca.getElementsByClass("vrow hybrid").forEach { araca_li ->
////// if (araca_li.html().contains("title ") == true) {
////// parseArcaLi(araca_li).apply {
////// this.forEach {
////// if (it.pubDate() > commicsDateTime) {
////// temp.add(it.getRssData())
////// }
////// }
////// }
////// }
////// }
////// }
////// } catch (e : Exception) {
////// e.printStackTrace()
////// }
////
//// }
////// Jsoup.connect("https://projrctjav.com").userAgent(USAGT).get().let { projectj ->
////// BLog.LOGE("projectj >>>>> ${projectj}")
////// }
//// } catch (e: Exception) {
//// e.printStackTrace()
//// }
//
// }
//// Jsoup.connect("https://projrctjav.com").userAgent(USAGT).get().let { projectj ->
//// BLog.LOGE("projectj >>>>> ${projectj}")
//// }
// } catch (e: Exception) {
// e.printStackTrace()
// return Result.success().apply {
//// WorkersDb.insertBulkData(temp)
// }
// }
return Result.success().apply {
// WorkersDb.insertBulkData(temp)
}
}
private fun parseArcaLi(aracaLi: Element) : ArrayList<RssDataInterface> {
var tempArray = arrayListOf<RssDataInterface>()
// BLog.LOGE("aracaLi >>> ${aracaLi}")
var title = aracaLi.getElementsByClass("title hybrid-title").getT()
var desc = aracaLi.getElementsByClass("badge").getT()
desc.plus(aracaLi.getElementsByClass("user-info ").getT())
var dateTime = aracaLi.getElementsByTag("time").attr("datetime")
var tumbnail = aracaLi.getElementsByTag("img").attr("src")
var link = "https://arca.live".plus(if(aracaLi.getElementsByClass("title hybrid-title").size > 0) aracaLi.getElementsByClass("title hybrid-title").get(0).attr("href") else if(aracaLi.getElementsByTag("a").size > 0) aracaLi.getElementsByTag("a").get(0).attr("href") else "")
if (title.length > 0 && link.length > 20) {
// if(blockKeyword.filter { desc.contains(it) }.size == 0) {
Arca().apply {
this.link = link
this.title = title
if (tumbnail.length > 0) {
this.thumbnail = "https:".plus(tumbnail)
//
// BLog.LOGE("Arca thumbnail >>> ${thumbnail}")
}
this.desc = desc
this.dateTiem = dateTime
}.apply {
// BLog.LOGE("parseArcaLi >>>> ${this}")
if (this.pubDate() > beforeOneDay()) {
tempArray.add(this)
}
}
// private fun parseArcaLi(aracaLi: Element) : ArrayList<RssDataInterface> {
// var tempArray = arrayListOf<RssDataInterface>()
//// BLog.LOGE("aracaLi >>> ${aracaLi}")
// var title = aracaLi.getElementsByClass("title hybrid-title").getT()
// var desc = aracaLi.getElementsByClass("badge").getT()
// desc.plus(aracaLi.getElementsByClass("user-info ").getT())
// var dateTime = aracaLi.getElementsByTag("time").attr("datetime")
// var tumbnail = aracaLi.getElementsByTag("img").attr("src")
// var link = "https://arca.live".plus(if(aracaLi.getElementsByClass("title hybrid-title").size > 0) aracaLi.getElementsByClass("title hybrid-title").get(0).attr("href") else if(aracaLi.getElementsByTag("a").size > 0) aracaLi.getElementsByTag("a").get(0).attr("href") else "")
// if (title.length > 0 && link.length > 20) {
//// if(blockKeyword.filter { desc.contains(it) }.size == 0) {
// Arca().apply {
// this.link = link
// this.title = title
// if (tumbnail.length > 0) {
// this.thumbnail = "https:".plus(tumbnail)
////
//// BLog.LOGE("Arca thumbnail >>> ${thumbnail}")
// }
// this.desc = desc
// this.dateTiem = dateTime
// }.apply {
//// BLog.LOGE("parseArcaLi >>>> ${this}")
// if (this.pubDate() > beforeOneDay()) {
// tempArray.add(this)
// }
// }
//// }
// }
// return tempArray
// }
//
//
//}
}
return tempArray
}
}

View File

@ -8,10 +8,11 @@ import bums.lunatic.launcher.LunaticLauncher
import bums.lunatic.launcher.model.RssData
import bums.lunatic.launcher.utils.beforeDay
import bums.lunatic.launcher.utils.beforeOneDay
import io.realm.kotlin.types.RealmObject
import java.util.Calendar
import java.util.Date
open abstract class BaseGetter : Worker {
abstract class BaseGetter(internal val context: Context) {
protected companion object {
var lastedUpdateTime = 0L
val defaultDay = 3
@ -29,21 +30,8 @@ open abstract class BaseGetter : Worker {
val commicsDateTime = beforeDay(1)
val temp = arrayListOf<RssData>()
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@CallSuper
override fun doWork(): Result {
LunaticLauncher.mHourlyLogWriter?.writeLog("${this::class.java.simpleName} doWork()")
val currentTime = before10Min()
if (lastedUpdateTime > 0L && currentTime > lastedUpdateTime) {
return Result.success().apply {
abstract fun realWork() : List<RealmObject>
open suspend fun fetchData(): List<RealmObject> {
return realWork()
}
}
return realWork().apply {
LunaticLauncher.mHourlyLogWriter?.writeLog("${this@BaseGetter::class.java.simpleName} return realWork() ")
}
}
abstract fun realWork() : Result
}

View File

@ -1,149 +1,149 @@
package bums.lunatic.launcher.workers
import android.content.Context
import android.net.Uri
import androidx.work.WorkerParameters
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.utils.Blog
class CalendarGetter : BaseGetter {
companion object {
val TAG = "DCGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
override fun realWork(): Result {
setCalendar()
return Result.success().apply {
}
}
fun setCalendar() {
val calendars = Uri.parse("content://com.android.calendar/events")
val projection = arrayOf(
"calendar_id",
// "htmlUri",
"title",
// "eventLocation",
"description",
// "eventStatus",
// "selfAttendeeStatus",
// "commentsUri",
"dtstart",
"dtend",
// "eventTimezone",
// "duration",
// "allDay",
// "visibility",
// "transparency",
// "hasAlarm",
// "hasExtendedProperties",
// "rrule",
"rdate",
// "exrule",
// "exdate",
// "originalEvent",
// "originalInstanceTime",
// "originalAllDay",
// "lastDate",
// "hasAttendeeData",
// "guestsCanModify",
// "guestsCanInviteOthers",
// "guestsCanSeeGuests",
// "organizer",
// "deleted"
)
// val managedCursor: Cursor =
lActivity?.contentResolver?.query(calendars, projection, null, null, null)?.let { managedCursor ->
if (managedCursor.moveToFirst()) {
val calendar_id = IntArray(managedCursor.count)
// val htmlUri = arrayOfNulls<String>(managedCursor.count)
val title = arrayOfNulls<String>(managedCursor.count)
// val eventLocation = arrayOfNulls<String>(managedCursor.count)
val description = arrayOfNulls<String>(managedCursor.count)
// val eventStatus = IntArray(managedCursor.count)
// val selfAttendeeStatus = IntArray(managedCursor.count)
// val commentsUri = arrayOfNulls<String>(managedCursor.count)
val dtstart = arrayOfNulls<String>(managedCursor.count)
val dtend = arrayOfNulls<String>(managedCursor.count)
// val eventTimezone = arrayOfNulls<String>(managedCursor.count)
// val duration = arrayOfNulls<String>(managedCursor.count)
// val allDay = IntArray(managedCursor.count)
// val visibility = IntArray(managedCursor.count)
// val transparency = IntArray(managedCursor.count)
// val hasAlarm = IntArray(managedCursor.count)
// val hasExtendedProperties = IntArray(managedCursor.count)
// val rrule = arrayOfNulls<String>(managedCursor.count)
val rdate = arrayOfNulls<String>(managedCursor.count)
// val exrule = arrayOfNulls<String>(managedCursor.count)
// val exdate = arrayOfNulls<String>(managedCursor.count)
// val originalEvent = arrayOfNulls<String>(managedCursor.count)
// val originalInstanceTime = IntArray(managedCursor.count)
// val originalAllDay = IntArray(managedCursor.count)
// val lastDate = IntArray(managedCursor.count)
// val hasAttendeeData = IntArray(managedCursor.count)
// val guestsCanModify = IntArray(managedCursor.count)
// val guestsCanInviteOthers = IntArray(managedCursor.count)
// val guestsCanSeeGuests = IntArray(managedCursor.count)
// val organizer = arrayOfNulls<String>(managedCursor.count)
// val deleted = IntArray(managedCursor.count)
for (i in title.indices) {
calendar_id[i] = managedCursor.getInt(0)
Blog.LOGE("Calendar ID : " + calendar_id[i])
// htmlUri[i] = managedCursor.getString(1)
// Log.i("Calendar", "htmlUri : " + htmlUri[i])
title[i] = managedCursor.getString(1)
Blog.LOGE("Calendar title : " + title[i])
// eventLocation[i] = managedCursor.getString(3)
// Log.i("Calendar", "eventLocation : " + eventLocation[i])
description[i] = managedCursor.getString(2)
// eventStatus[i] = managedCursor.getInt(5)
// selfAttendeeStatus[i] = managedCursor.getInt(6)
// commentsUri[i] = managedCursor.getString(7)
dtstart[i] = managedCursor.getString(3)
Blog.LOGE("Calendar dtstart : " + rdate[i])
dtend[i] = managedCursor.getString(4)
Blog.LOGE("Calendar dtend : " + rdate[i])
// eventTimezone[i] = managedCursor.getString(10)
// duration[i] = managedCursor.getString(11)
// allDay[i] = managedCursor.getInt(12)
// visibility[i] = managedCursor.getInt(13)
// transparency[i] = managedCursor.getInt(14)
// hasAlarm[i] = managedCursor.getInt(15)
// hasExtendedProperties[i] = managedCursor.getInt(16)
// rrule[i] = managedCursor.getString(17)
rdate[i] = managedCursor.getString(5)
Blog.LOGE("Calendar rdate : " + rdate[i])
// exrule[i] = managedCursor.getString(19)
// exdate[i] = managedCursor.getString(20)
// originalEvent[i] = managedCursor.getString(21)
// originalInstanceTime[i] = managedCursor.getInt(22)
// originalAllDay[i] = managedCursor.getInt(23)
// lastDate[i] = managedCursor.getInt(24)
// hasAttendeeData[i] = managedCursor.getInt(25)
// guestsCanModify[i] = managedCursor.getInt(26)
// guestsCanInviteOthers[i] = managedCursor.getInt(27)
// guestsCanSeeGuests[i] = managedCursor.getInt(28)
// organizer[i] = managedCursor.getString(29)
// deleted[i] = managedCursor.getInt(30)
if (title[i] != null) {
Blog.LOGE("title[i] ${title[i]}")
}
managedCursor.moveToNext()
}
}
managedCursor.close()
}
}
}
//package bums.lunatic.launcher.workers
//
//import android.content.Context
//import android.net.Uri
//import androidx.work.WorkerParameters
//import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
//import bums.lunatic.launcher.utils.Blog
//import io.realm.kotlin.types.RealmObject
//
//
//class CalendarGetter : BaseGetter {
// companion object {
// val TAG = "DCGetter"
// }
// constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
//
// }
//
//
// override fun realWork(): List<RealmObject> {
// return setCalendar()
// }
//
// fun setCalendar() {
//
//
// val calendars = Uri.parse("content://com.android.calendar/events")
//
// val projection = arrayOf(
// "calendar_id",
//// "htmlUri",
// "title",
//// "eventLocation",
// "description",
//// "eventStatus",
//// "selfAttendeeStatus",
//// "commentsUri",
// "dtstart",
// "dtend",
//// "eventTimezone",
//// "duration",
//// "allDay",
//// "visibility",
//// "transparency",
//// "hasAlarm",
//// "hasExtendedProperties",
//// "rrule",
// "rdate",
//// "exrule",
//// "exdate",
//// "originalEvent",
//// "originalInstanceTime",
//// "originalAllDay",
//// "lastDate",
//// "hasAttendeeData",
//// "guestsCanModify",
//// "guestsCanInviteOthers",
//// "guestsCanSeeGuests",
//// "organizer",
//// "deleted"
// )
//// val managedCursor: Cursor =
// lActivity?.contentResolver?.query(calendars, projection, null, null, null)?.let { managedCursor ->
// if (managedCursor.moveToFirst()) {
// val calendar_id = IntArray(managedCursor.count)
//
//// val htmlUri = arrayOfNulls<String>(managedCursor.count)
// val title = arrayOfNulls<String>(managedCursor.count)
//// val eventLocation = arrayOfNulls<String>(managedCursor.count)
// val description = arrayOfNulls<String>(managedCursor.count)
//// val eventStatus = IntArray(managedCursor.count)
//// val selfAttendeeStatus = IntArray(managedCursor.count)
//// val commentsUri = arrayOfNulls<String>(managedCursor.count)
// val dtstart = arrayOfNulls<String>(managedCursor.count)
// val dtend = arrayOfNulls<String>(managedCursor.count)
//// val eventTimezone = arrayOfNulls<String>(managedCursor.count)
//// val duration = arrayOfNulls<String>(managedCursor.count)
//// val allDay = IntArray(managedCursor.count)
//// val visibility = IntArray(managedCursor.count)
//// val transparency = IntArray(managedCursor.count)
//// val hasAlarm = IntArray(managedCursor.count)
//// val hasExtendedProperties = IntArray(managedCursor.count)
//// val rrule = arrayOfNulls<String>(managedCursor.count)
// val rdate = arrayOfNulls<String>(managedCursor.count)
//// val exrule = arrayOfNulls<String>(managedCursor.count)
//// val exdate = arrayOfNulls<String>(managedCursor.count)
//// val originalEvent = arrayOfNulls<String>(managedCursor.count)
//// val originalInstanceTime = IntArray(managedCursor.count)
//// val originalAllDay = IntArray(managedCursor.count)
//// val lastDate = IntArray(managedCursor.count)
//// val hasAttendeeData = IntArray(managedCursor.count)
//// val guestsCanModify = IntArray(managedCursor.count)
//// val guestsCanInviteOthers = IntArray(managedCursor.count)
//// val guestsCanSeeGuests = IntArray(managedCursor.count)
//// val organizer = arrayOfNulls<String>(managedCursor.count)
//// val deleted = IntArray(managedCursor.count)
//
// for (i in title.indices) {
// calendar_id[i] = managedCursor.getInt(0)
// Blog.LOGE("Calendar ID : " + calendar_id[i])
//// htmlUri[i] = managedCursor.getString(1)
//// Log.i("Calendar", "htmlUri : " + htmlUri[i])
// title[i] = managedCursor.getString(1)
// Blog.LOGE("Calendar title : " + title[i])
//// eventLocation[i] = managedCursor.getString(3)
//// Log.i("Calendar", "eventLocation : " + eventLocation[i])
// description[i] = managedCursor.getString(2)
//// eventStatus[i] = managedCursor.getInt(5)
//// selfAttendeeStatus[i] = managedCursor.getInt(6)
//// commentsUri[i] = managedCursor.getString(7)
// dtstart[i] = managedCursor.getString(3)
// Blog.LOGE("Calendar dtstart : " + rdate[i])
// dtend[i] = managedCursor.getString(4)
// Blog.LOGE("Calendar dtend : " + rdate[i])
//// eventTimezone[i] = managedCursor.getString(10)
//// duration[i] = managedCursor.getString(11)
//// allDay[i] = managedCursor.getInt(12)
//// visibility[i] = managedCursor.getInt(13)
//// transparency[i] = managedCursor.getInt(14)
//// hasAlarm[i] = managedCursor.getInt(15)
//// hasExtendedProperties[i] = managedCursor.getInt(16)
//// rrule[i] = managedCursor.getString(17)
// rdate[i] = managedCursor.getString(5)
// Blog.LOGE("Calendar rdate : " + rdate[i])
//// exrule[i] = managedCursor.getString(19)
//// exdate[i] = managedCursor.getString(20)
//// originalEvent[i] = managedCursor.getString(21)
//// originalInstanceTime[i] = managedCursor.getInt(22)
//// originalAllDay[i] = managedCursor.getInt(23)
//// lastDate[i] = managedCursor.getInt(24)
//// hasAttendeeData[i] = managedCursor.getInt(25)
//// guestsCanModify[i] = managedCursor.getInt(26)
//// guestsCanInviteOthers[i] = managedCursor.getInt(27)
//// guestsCanSeeGuests[i] = managedCursor.getInt(28)
//// organizer[i] = managedCursor.getString(29)
//// deleted[i] = managedCursor.getInt(30)
//
// if (title[i] != null) {
// Blog.LOGE("title[i] ${title[i]}")
// }
//
// managedCursor.moveToNext()
// }
// }
// managedCursor.close()
// }
// }
//
//}

View File

@ -9,14 +9,14 @@ import bums.lunatic.launcher.model.getHref
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.model.getT
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.types.RealmObject
import org.jsoup.Jsoup
class ClienGetter : BaseGetter {
class ClienGetter(context: Context) : BaseGetter(context = context) {
companion object {
val TAG = "ClienGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
fun parseClien(div_clien : org.jsoup.nodes.Element) {
// BLog.LOGE("div_clien >>>> ${div_clien}")
@ -44,33 +44,12 @@ class ClienGetter : BaseGetter {
}
}
}
// var desc = tq_tr.getElementsByClass("cate").getT()
// var title = tq_tr.getElementsByClass("title").getT()
// var pageLink = tq_tr.getElementsByTag("a").getHref()
// var dateTime = tq_tr.getElementsByClass("time").getT()
// BLog.LOGE("${TAG} :::: desc >>> $desc")
// BLog.LOGE("${TAG} :::: title >>> $title")
// BLog.LOGE("${TAG} :::: pageLink >>> $pageLink")
// BLog.LOGE("${TAG} :::: dateTime >>> $dateTime")
// if (title.length > 0 && pageLink.length > 0) {
// TheQoo().let { tq ->
// tq.title = title
// tq.link = "https://theqoo.net".plus(pageLink)
// tq.dateTiem = dateTime
// tq.desc = desc
// if (tq.pubDate() > limitDateTime) {
// temp.add(tq.getRssData())
// }
// }
// }
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
RssDataType.CLIEN.isOn {
Blog.LOGE("realWork() ${this::class.simpleName}")
try {
@ -90,8 +69,6 @@ class ClienGetter : BaseGetter {
e.printStackTrace()
}
}
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -4,17 +4,18 @@ import android.content.Context
import android.provider.ContactsContract
import androidx.work.WorkerParameters
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.apps.SimpleContact
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.ext.query
import io.realm.kotlin.types.RealmObject
class ContactInfoGetter : BaseGetter {
class ContactInfoGetter(context: Context) : BaseGetter(context) {
companion object {
val TAG = "ContactInfoGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
var temp = mutableListOf<SimpleContact>()
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
@ -23,28 +24,31 @@ class ContactInfoGetter : BaseGetter {
)
try {
val cursor = lActivity?.contentResolver?.query(phoneUri, projection, null, null, null)
if (cursor != null) {
lActivity?.contentResolver?.query(phoneUri, projection, null, null, null)?.use { cursor ->
val idIdx = cursor.getColumnIndex(projection[0])
val nameIdx = cursor.getColumnIndex(projection[1])
val numberIdx = cursor.getColumnIndex(projection[2])
while (cursor.moveToNext()) {
val idx =cursor.getColumnIndex(projection[0])
val nameIndex = cursor.getColumnIndex(projection[1])
val numberIndex = cursor.getColumnIndex(projection[2])
var contactId = cursor.getString(idx)
val name = cursor.getString(nameIndex)
var number = cursor.getString(numberIndex)
number = number.replace("-", "")
if (name?.length ?: 0 > 0 && number?.length ?: 0 > 0) {
if (WorkersDb.getRealm().query<SimpleContact>("id == $0", contactId).find().size == 0) {
WorkersDb.update(SimpleContact(contactId,name,number))
val contactId = cursor.getString(idIdx)
val name = cursor.getString(nameIdx)
// 루프 내 불필요한 객체 생성 방지 및 null 처리
val rawNumber = cursor.getString(numberIdx)
val number = rawNumber?.replace("-", "") ?: ""
if (!name.isNullOrEmpty() && number.isNotEmpty()) {
temp.add(SimpleContact(contactId, name, number))
}
}
}
}
// 데이터 계열은 반드시 닫아줘야 한다.
cursor?.close()
} catch (e: Exception) {
e.printStackTrace()
}
return Result.success()
Blog.LOGE("ContactInfoGetter >>> ${temp.size}")
return temp
}
}

View File

@ -9,15 +9,15 @@ import bums.lunatic.launcher.model.RssDataInterface
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.types.RealmObject
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
class DCGetter : BaseGetter {
class DCGetter(context: Context) : BaseGetter(context) {
companion object {
val TAG = "DCGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
fun parseDcLi(dc_li : org.jsoup.nodes.Element) : ArrayList<RssDataInterface>{
var temp = arrayListOf<RssDataInterface>()
@ -71,7 +71,7 @@ class DCGetter : BaseGetter {
}
val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
// Blog.LOGE("${TAG} RssDataType.DCINSIDE.isOn >>>> ${PrefHelper.getBoolean(RssDataType.DCINSIDE.name,false)}")
RssDataType.DCINSIDE.isOn {
Blog.LOGE("realWork() ${this::class.simpleName}")
@ -161,12 +161,6 @@ class DCGetter : BaseGetter {
e.printStackTrace()
}
}
return Result.success().apply {
try {
WorkersDb.insertBulkData(temp)
} catch (e : Exception) {
}
}
return temp
}
}

View File

@ -6,30 +6,24 @@ import androidx.work.WorkerParameters
import bums.lunatic.launcher.LauncherActivity
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.types.RealmObject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DotaxGetter : BaseGetter {
class DotaxGetter(context: Context) : BaseGetter(context) {
companion object {
val COMIC2_WORK_TAG = "ComicGetter2"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
RssDataType.DOTAX.isOn {
try {
Blog.LOGE("realWork() ${this::class.simpleName}")
temp.clear()
// val dotaxUrls = arrayListOf("https://cafe.daum.net/dotax",
// "https://m.cafe.daum.net/dotax/Elgq"
//// "https://m.cafe.daum.net/dotax/_rec?page=3"
// )
// dotaxUrls?.forEach { url ->
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.Main) {
LauncherActivity.lActivity?.let {
@ -37,32 +31,8 @@ class DotaxGetter : BaseGetter {
}
}
}
// }
// dotaxUrls?.forEach {
// Jsoup.connect(it).timeout(3000).userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36").get()?.let { dotax ->
// Blog.LOGE("dotax.html() >>> ${dotax.html()}")
// dotax.getElementsByTag("tr").forEach { dotax_li ->
// Blog.LOGE("dotax_li >>> ${dotax_li.text()}")
// if (dotax_li.getElementsByTag("a").size > 0) {
// val pageLink = dotax_li.getElementsByTag("a").get(0).attr("href")
// val desc = dotax_li.getElementsByClass("board_name").text()
// val dateTime = dotax_li.getElementsByClass("created_at").text()
// val title = dotax_li.getElementsByClass("txt_detail").text()
// val thumbnail = dotax_li.getElementsByClass("article_thumb").text()
// if (pageLink.length > 0 && desc.length > 0 && dateTime.length > 0 && title.length > 0) {
// Dotax(pageLink, desc, dateTime, title, thumbnail).let { dotax ->
// if(dotax.pubDate() > commicsDateTime) {
// temp.add(dotax.getRssData())
// }
// }
// }
// }
// }
// }
// }
} catch (e : Exception) {e.printStackTrace()}}
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -7,16 +7,14 @@ import bums.lunatic.launcher.model.FmKorea
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.types.RealmObject
import org.jsoup.Jsoup
import java.util.Date
class FmKoreaGetter : BaseGetter {
class FmKoreaGetter (context: Context): BaseGetter(context) {
companion object {
val FM_WORK_TAG = "FmKoreaGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
fun extractDocumentSrl(url: String): String? {
val uri = java.net.URI(url)
@ -33,7 +31,8 @@ class FmKoreaGetter : BaseGetter {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
temp.clear()
RssDataType.FMKORAE.isOn {
val now = Date()
try {
@ -97,8 +96,6 @@ class FmKoreaGetter : BaseGetter {
e.printStackTrace()
}
}
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -1,54 +1,55 @@
package bums.lunatic.launcher.workers
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import androidx.work.WorkerParameters
import bums.lunatic.launcher.common.letTrue
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.LocationUpdateService.Companion.pushLocation
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource
//
//import android.annotation.SuppressLint
//import android.content.Context
//import android.location.Location
//import androidx.work.WorkerParameters
//import bums.lunatic.launcher.common.letTrue
//import bums.lunatic.launcher.helpers.PrefBoolean
//import bums.lunatic.launcher.utils.Blog
//import bums.lunatic.launcher.workers.LocationUpdateService.Companion.pushLocation
//import com.google.android.gms.location.LocationServices
//import com.google.android.gms.location.Priority
//import com.google.android.gms.tasks.CancellationTokenSource
//import io.realm.kotlin.types.RealmObject
import kotlin.math.cos
class LocationGetter(context: Context, workerParams: WorkerParameters) : BaseGetter(context, workerParams) {
companion object {
val TAG = "LocationGetter"
var longitude: Double = 0.0
var latitude: Double = 0.0
}
@SuppressLint("MissingPermission")
override fun realWork(): Result {
// Blog.LOGE("${OpenWeatherGetter.TAG} realWork()")
LocationServices.getFusedLocationProviderClient(this.applicationContext)
.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token)
.addOnSuccessListener{ success: Location? ->
success?.let {
Blog.LOGE("Location >>> $it")
Blog.LOGE("Location >>> (latitude)${it.longitude}/(longitude)${it.latitude}")
longitude = it.longitude
latitude = it.latitude
// runWeatherGetter()
PrefBoolean.location.get().letTrue {
pushLocation(this.applicationContext,it.latitude, it.longitude)
}
}
}.addOnFailureListener{
Blog.LOGE("Location error >>> $it")
}
return Result.success()
}
}
//
//class LocationGetter(context: Context) : BaseGetter(context) {
// companion object {
// val TAG = "LocationGetter"
// var longitude: Double = 0.0
// var latitude: Double = 0.0
// }
//
// @SuppressLint("MissingPermission")
// override fun realWork(): List<RealmObject> {
//// Blog.LOGE("${OpenWeatherGetter.TAG} realWork()")
//
// LocationServices.getFusedLocationProviderClient(context)
// .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token)
// .addOnSuccessListener{ success: Location? ->
// success?.let {
// Blog.LOGE("Location >>> $it")
// Blog.LOGE("Location >>> (latitude)${it.longitude}/(longitude)${it.latitude}")
// longitude = it.longitude
// latitude = it.latitude
//// runWeatherGetter()
// PrefBoolean.location.get().letTrue {
// pushLocation(context,it.latitude, it.longitude)
// }
// }
// }.addOnFailureListener{
// Blog.LOGE("Location error >>> $it")
// }
//
// return temp
// }
//
//
//
//}
//
//
val EARTH_RADIUS_METERS = 6371000
val LATITUDE_DEGREE_PER_METER: Double = 1.0 / (2 * Math.PI * EARTH_RADIUS_METERS / 360)
@ -70,8 +71,8 @@ fun longitudeRange(latitude: Double, longitude: Double, radiusInMeters: Int): Do
return doubleArrayOf(minLongitude, maxLongitude)
}
//https://jinkpark.tistory.com/296
//https://develoyummer.tistory.com/103
//https://ghj1001020.tistory.com/300
//
//
////https://jinkpark.tistory.com/296
////https://develoyummer.tistory.com/103
////https://ghj1001020.tistory.com/300

View File

@ -18,8 +18,6 @@ import bums.lunatic.launcher.helpers.PrefString
import bums.lunatic.launcher.model.LocationLog
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.inRange
import bums.lunatic.launcher.workers.LocationGetter.Companion.latitude
import bums.lunatic.launcher.workers.LocationGetter.Companion.longitude
import com.google.android.gms.location.LocationServices
import com.google.gson.Gson
import io.realm.kotlin.ext.query
@ -42,6 +40,8 @@ import java.util.concurrent.TimeUnit
class LocationUpdateService : Service(), LocationListener {
companion object {
var longitude: Double = 0.0
var latitude: Double = 0.0
fun pushLocation(context: Context, lat :Double, long : Double) {
try {
Blog.LOGE("Location >>> ${lat},${long}")
@ -179,7 +179,6 @@ class LocationUpdateService : Service(), LocationListener {
).show()
longitude = location.longitude
latitude = location.latitude
// runWeatherGetter()
pushLocation(this.applicationContext, location.latitude, location.longitude)
}
}

View File

@ -7,17 +7,17 @@ import bums.lunatic.launcher.home.adapters.RssFeedsParser
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.utils.RssList
import io.realm.kotlin.types.RealmObject
class NewsFeedsGetter : BaseGetter {
class NewsFeedsGetter(context: Context) : BaseGetter(context) {
companion object {
val FEDDS_WORK_TAG = "NewsFeedsGetter"
}
var feddsUrls = arrayListOf<String>()
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
RssDataType.NEWSFEED.isOn {
feddsUrls.clear()
feddsUrls.addAll(RssList.newsFeeds)
@ -32,15 +32,7 @@ class NewsFeedsGetter : BaseGetter {
}
}
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
// temp.forEach { synchronized(rssSet){
// rssSet.put(it.originPage(), it)
// } }.run {
// rssSetTouchCount -= 1
// Result.success() }
return temp
}

View File

@ -1,89 +1,90 @@
package bums.lunatic.launcher.workers
import android.content.Context
import androidx.work.WorkerParameters
import bums.lunatic.launcher.helpers.PrefString
import bums.lunatic.launcher.model.WeatherForcast
import bums.lunatic.launcher.model.WeatherInfoManager
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.UpdatePolicy
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
class OpenWeatherGetter(context: Context, workerParams: WorkerParameters) : BaseGetter(context, workerParams) {
companion object {
val TAG = "OpenWeatherGetter"
var lon: Double? = null // 경도
var lat: Double? = null // 위도
}
//////////////////////////////////////////
// weatherapi
val VER_WEATHERAPI = "v1"
val URI_WEATHERAPI = "https://api.weatherapi.com"
val KEY_WEATHERAPI = "8133d83d23ab4175a4160624241909"
val DAYS = 3
//////////////////////////////////////////
//////////////////////////////////////////
override fun realWork(): Result {
Blog.LOGE("${TAG} realWork()")
// 위치 값 가져오기
lat = LocationGetter.latitude
lon = LocationGetter.longitude
if (lat != null && lon != null) {
getWeather(lat!!, lon!!)
} else {
Blog.LOGE("lat or lon is null")
}
return Result.success()
}
fun getWeather(latitude: Double, longitude: Double) {
Blog.LOGE("into getWeather ${PrefString.weatherApiKey.get()}")
///saved weatherForcast
Retrofit.Builder()
.baseUrl(URI_WEATHERAPI)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create<RestrofitService>()
.getForecast( // weatherApi
ver = VER_WEATHERAPI,
key = PrefString.weatherApiKey.get(),
q = "$latitude,$longitude",
days = (System.currentTimeMillis() % 5L).toInt().toString()
)?.execute()?.let { response ->
// BLog.LOGE("into getWeather after execute")
// BLog.LOGE("weatherApi forecast response >>> $response")
response.body()?.let { weatherInfo ->
WeatherInfoManager.info = weatherInfo
WeatherInfoManager.readyForSaving(lat ?: 0.0, lon ?: 0.0)
// Realm에 저장
WorkersDb.getRealm().writeBlocking {
copyToRealm(weatherInfo, UpdatePolicy.ALL).also {
// BLog.LOGE("saved weatherForcast >>> $it")
}
}
// BLog.LOGE("saved weatherForcast forecastdayRealm.size >>> ${WorkersDb.getRealm().query<WeatherForcast>().first().find()?.forecast?.forecastdayRealm?.size}")
// BLog.LOGE("saved weatherForcast hour.count >>> ${WorkersDb.getRealm().query<Hour>().count().find()}")
}
}
}
}
interface RestrofitService {
// weather_api
@GET("/{ver}/forecast.json")
fun getForecast(
@Path("ver") ver: String,
@Query("key") key: String,
@Query("q") q: String,
@Query("days") days: String
): Call<WeatherForcast?>?
}
//package bums.lunatic.launcher.workers
//
//import android.content.Context
//import androidx.work.WorkerParameters
//import bums.lunatic.launcher.helpers.PrefString
//import bums.lunatic.launcher.model.WeatherForcast
//import bums.lunatic.launcher.model.WeatherInfoManager
//import bums.lunatic.launcher.utils.Blog
//import io.realm.kotlin.UpdatePolicy
//import io.realm.kotlin.types.RealmObject
//import retrofit2.Call
//import retrofit2.Retrofit
//import retrofit2.converter.gson.GsonConverterFactory
//import retrofit2.create
//import retrofit2.http.GET
//import retrofit2.http.Path
//import retrofit2.http.Query
//
//class OpenWeatherGetter(context: Context) : BaseGetter(context) {
// companion object {
// val TAG = "OpenWeatherGetter"
// var lon: Double? = null // 경도
// var lat: Double? = null // 위도
// }
// //////////////////////////////////////////
// // weatherapi
// val VER_WEATHERAPI = "v1"
// val URI_WEATHERAPI = "https://api.weatherapi.com"
// val KEY_WEATHERAPI = "8133d83d23ab4175a4160624241909"
// val DAYS = 3
// //////////////////////////////////////////
//
// //////////////////////////////////////////
//
// override fun realWork(): List<RealmObject> {
// Blog.LOGE("${TAG} realWork()")
// // 위치 값 가져오기
// lat = LocationGetter.latitude
// lon = LocationGetter.longitude
// if (lat != null && lon != null) {
// getWeather(lat!!, lon!!)
// } else {
// Blog.LOGE("lat or lon is null")
// }
// return Result.success()
// }
//
// fun getWeather(latitude: Double, longitude: Double) {
// Blog.LOGE("into getWeather ${PrefString.weatherApiKey.get()}")
// ///saved weatherForcast
// Retrofit.Builder()
// .baseUrl(URI_WEATHERAPI)
// .addConverterFactory(GsonConverterFactory.create())
// .build()
// .create<RestrofitService>()
// .getForecast( // weatherApi
// ver = VER_WEATHERAPI,
// key = PrefString.weatherApiKey.get(),
// q = "$latitude,$longitude",
// days = (System.currentTimeMillis() % 5L).toInt().toString()
// )?.execute()?.let { response ->
//// BLog.LOGE("into getWeather after execute")
//// BLog.LOGE("weatherApi forecast response >>> $response")
// response.body()?.let { weatherInfo ->
// WeatherInfoManager.info = weatherInfo
// WeatherInfoManager.readyForSaving(lat ?: 0.0, lon ?: 0.0)
// // Realm에 저장
// WorkersDb.getRealm().writeBlocking {
// copyToRealm(weatherInfo, UpdatePolicy.ALL).also {
//// BLog.LOGE("saved weatherForcast >>> $it")
// }
// }
//// BLog.LOGE("saved weatherForcast forecastdayRealm.size >>> ${WorkersDb.getRealm().query<WeatherForcast>().first().find()?.forecast?.forecastdayRealm?.size}")
//// BLog.LOGE("saved weatherForcast hour.count >>> ${WorkersDb.getRealm().query<Hour>().count().find()}")
// }
// }
// }
//}
//
//interface RestrofitService {
// // weather_api
// @GET("/{ver}/forecast.json")
// fun getForecast(
// @Path("ver") ver: String,
// @Query("key") key: String,
// @Query("q") q: String,
// @Query("days") days: String
// ): Call<WeatherForcast?>?
//}
//

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.provider.CallLog
import androidx.work.WorkerParameters
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.apps.SimpleContact
import bums.lunatic.launcher.utils.beforeDay
import bums.lunatic.launcher.utils.getContactId
import com.google.gson.Gson
@ -48,18 +47,16 @@ class RecentCall : RealmObject {
}
class RecentCallGetter : BaseGetter {
class RecentCallGetter(context: Context) : BaseGetter(context) {
companion object{
var dayRange = BaseGetter.defaultDay
val TAG = "RecentCallGetter"
val dateFormat = SimpleDateFormat("yyy/MM/dd-HH:mm:ss")
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
var temp = mutableListOf<RecentCall>()
var dateParam = beforeDay(dayRange).toString()
var managedCursor = lActivity?.contentResolver?.query(
CallLog.Calls.CONTENT_URI, arrayOf(
@ -69,8 +66,7 @@ class RecentCallGetter : BaseGetter {
CallLog.Calls.DURATION,
CallLog.Calls.CACHED_NAME,
), CallLog.Calls.DATE + " >= ? " , arrayOf<String>(dateParam), CallLog.Calls.DATE + " desc")
//+ CallLog.Calls.TYPE + " > ?"
//, "2"
if(managedCursor != null && managedCursor.isClosed == false) {
try {
val number = managedCursor.getColumnIndex(CallLog.Calls.NUMBER)
@ -97,8 +93,7 @@ class RecentCallGetter : BaseGetter {
CallLog.Calls.BLOCKED_TYPE -> { dir = "BLOCKED_TYPE" }
CallLog.Calls.ANSWERED_EXTERNALLY_TYPE -> { dir = "ANSWERED_EXTERNALLY_TYPE" }
}
val call = RecentCall(
temp.add(RecentCall(
1,
callerName,
phNumber,
@ -107,27 +102,8 @@ class RecentCallGetter : BaseGetter {
dateFormat.format(Date(callDayTime)),
callDayTime,
callDuration.toLong()
)
call.uniqK?.let {
// BLog.LOGE("${TAG} call.uniqK? >>> ${call.uniqK}")
WorkersDb.getRealm().writeBlocking {
if(query<RecentCall>().query("uniqK == $0", it).find().count() < 1) {
this.copyToRealm(call, UpdatePolicy.ALL)
var cId = getContactId(lActivity!!.contentResolver, call.number)
if (cId!=null && cId.length > 0) {
val result = query<SimpleContact>().query("id == $0", cId).find()
if(result.size > 0){
var contact = result.first()
contact.touchCount = contact.touchCount + 1
contact.lastedTouchDateTime = Math.max(contact.lastedTouchDateTime, callDayTime)
// BLog.LOGE("${TAG} updatetouch info >>>> ${contact.touchCount}")
}
}
}
// BLog.LOGE("${TAG} updatetouch info >>>> BE ")
}
// BLog.LOGE("${TAG} updatetouch info >>>> E")
}
))
}
} catch (e: Exception) {
e.printStackTrace()
@ -135,9 +111,7 @@ class RecentCallGetter : BaseGetter {
managedCursor.close()
}
}
return Result.success().apply {
dayRange = BaseGetter.defaultDay
}
return temp
}
}

View File

@ -23,20 +23,16 @@ import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
class RecentSmsGetter : BaseGetter {
class RecentSmsGetter(context: Context) : BaseGetter(context) {
companion object {
var dayRange = BaseGetter.defaultDay
val SMS_WORK_TAG = "RecentSmsGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
var temp = mutableListOf<RecentSms>()
var dateParam = beforeDay(dayRange).toString()
val managedCursor = lActivity?.contentResolver?.query(
Telephony.Sms.CONTENT_URI, arrayOf(
@ -88,15 +84,8 @@ class RecentSmsGetter : BaseGetter {
)
log.isMms = false
// BLog.LOGE("RecentSmsGetter resultData put ${phNumber +"_"+ reciveDate} >>> ${log.toJson()}")
log.sender = getContactName(applicationContext.contentResolver,phNumber) ?: ""
WorkersDb.getRealm().apply {
if (query<RecentSms>("uniqKey == $0", log.uniqKey).find().size == 0) {
writeBlocking {
copyToRealm(log)
}
}
}
log.sender = getContactName(context.applicationContext.contentResolver,phNumber) ?: ""
temp.add(log)
}
} catch (e: Exception) {
@ -107,9 +96,7 @@ class RecentSmsGetter : BaseGetter {
if (lActivity?.contentResolver != null) {
MmsQueryHelper(lActivity?.contentResolver!!).query()
}
return Result.success().apply {
dayRange = BaseGetter.defaultDay
}
return temp
}
}

View File

@ -9,18 +9,17 @@ import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.utils.RssList.feedJsons
import bums.lunatic.launcher.utils.RssList.feedJsons_nsfw
import io.realm.kotlin.types.RealmObject
class RedditGetter : BaseGetter {
class RedditGetter(context: Context) : BaseGetter(context) {
companion object{
val REDDIT_WORK_TAG = "RedditGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
val temp = arrayListOf<RssData>()
override fun realWork(): List<RealmObject> {
temp.clear()
RssDataType.REDDIT.isOn { for (url in feedJsons) {
for (it in RssFeedsParser.getReddit(url,false)) {
if (it.pubDate() >= limitDateTime) {
@ -37,8 +36,6 @@ class RedditGetter : BaseGetter {
}
} }
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -8,14 +8,13 @@ import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.RuliWeb
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.types.RealmObject
import org.jsoup.Jsoup
class RuliWebGetter : BaseGetter {
class RuliWebGetter(context: Context) : BaseGetter(context) {
companion object {
val TAG = "RuliWebGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
fun parseRuli(ruli_tr : org.jsoup.nodes.Element) {
Blog.LOGE("ruli_tr >>> ${ruli_tr.text()}")
@ -61,7 +60,8 @@ class RuliWebGetter : BaseGetter {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
temp.clear()
RssDataType.RULIWEB.isOn {
try {
Blog.LOGE("realWork() ${this::class.simpleName}")
@ -82,9 +82,6 @@ class RuliWebGetter : BaseGetter {
}
}
} catch (e:Exception){e.printStackTrace()}}
return Result.success().apply {
// BLog.LOGE("Ruli temp >>>> ${temp.size}")
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -0,0 +1,130 @@
package bums.lunatic.launcher.workers
import android.content.Context
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.RssData
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.utils.Blog
import io.realm.kotlin.UpdatePolicy
import kotlinx.coroutines.*
object TaskAggregator {
// 1. 시스템 정보 통합 업데이트 (App, Contact, SMS 등)
// 로컬 작업이므로 Dispatchers.Default 사용
fun refreshSystemInfo(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
// 1. [병렬 수집] 시스템에서 최신 데이터 가져오기 (이때 카운트는 0인 상태)
val appsJob = async { AppInfoGetter(context).fetchData() }
val contactsJob = async { ContactInfoGetter(context).fetchData() }
val scannedApps: List<AppInfo> = appsJob.await().filterIsInstance<AppInfo>()
val scannedContacts: List<SimpleContact> = contactsJob.await().filterIsInstance<SimpleContact>()
val realm = WorkersDb.getRealm()
// 2. [일괄 저장 및 병합] 트랜잭션 시작
realm.write {
// --- [A] 앱 정보 병합 (AppInfo) ---
// DB에 있는 기존 앱들을 패키지명(Key) 기준으로 Map 생성
val existingAppsMap = query<AppInfo>(AppInfo::class).find().associateBy { it.pkgName }
val activeAppPkgNames = HashSet<String>()
scannedApps.forEach { newApp ->
val pkgName = newApp.pkgName ?: return@forEach
activeAppPkgNames.add(pkgName)
// 기존 데이터가 있으면 카운트 정보 이식
val oldApp = existingAppsMap[pkgName]
if (oldApp != null) {
newApp.clickCount = oldApp.clickCount
newApp.lastUseDate = oldApp.lastUseDate
// 즐겨찾기 여부 등 보존해야 할 다른 필드가 있다면 여기서 복사
// newApp.isFavorite = oldApp.isFavorite
}
// 병합된 데이터 저장 (덮어쓰기)
copyToRealm(newApp, UpdatePolicy.ALL)
}
// 삭제된 앱 처리 (시스템 스캔 목록에 없는 앱은 DB에서 삭제)
val appsToDelete = query<AppInfo>(AppInfo::class).find().filter {
!activeAppPkgNames.contains(it.pkgName)
}
appsToDelete.forEach { delete(it) }
// --- [B] 연락처 정보 병합 (SimpleContact) ---
// DB에 있는 기존 연락처들을 ID(Key) 기준으로 Map 생성
// (SimpleContact의 PrimaryKey가 id라고 가정)
val existingContactsMap = query<SimpleContact>(SimpleContact::class).find().associateBy { it.id }
val activeContactIds = HashSet<String>()
scannedContacts.forEach { newContact ->
// SimpleContact 타입 캐스팅 (fetchData 반환형이 List<RealmObject>이므로)
val contact = newContact as? SimpleContact ?: return@forEach
val contactId = contact.id ?: return@forEach
activeContactIds.add(contactId)
val oldContact = existingContactsMap[contactId]
if (oldContact != null) {
contact.touchCount = oldContact.touchCount
contact.lastedTouchDateTime = oldContact.lastedTouchDateTime
}
copyToRealm(contact, UpdatePolicy.ALL)
}
// 삭제된 연락처 처리
val contactsToDelete = query<SimpleContact>(SimpleContact::class).find().filter {
!activeContactIds.contains(it.id)
}
contactsToDelete.forEach { delete(it) }
}
Blog.LOGE("SystemInfo Aggregation finished in ${System.currentTimeMillis() - startTime}ms.")
}
}
// 2. 뉴스 피드 통합 업데이트 (Ruliweb, Youtube, FMKorea 등)
// 네트워크 작업이므로 Dispatchers.IO 사용
fun refreshNewsFeeds(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
// 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨)
val jobs = listOf(
async { RuliWebGetter(context).fetchData() },
async { YoutubeGetter(context).fetchData() },
async { DCGetter(context).fetchData() },
async { FmKoreaGetter(context).fetchData() },
async { NewsFeedsGetter(context).fetchData() },
async { ClienGetter(context).fetchData() },
async { DotaxGetter(context).fetchData() },
async { RedditGetter(context).fetchData() },
)
// 결과 수집
val allFeeds = jobs.awaitAll().flatten()
// [일괄 저장]
if (allFeeds.isNotEmpty()) {
WorkersDb.getRealm().write {
// 1. 새 데이터 저장
allFeeds.forEach { feed ->
copyToRealm(feed, UpdatePolicy.ALL)
}
}
}
Blog.LOGE("NewsFeed Aggregation finished in ${System.currentTimeMillis() - startTime}ms. Items: ${allFeeds.size}")
}
}
}

View File

@ -1,141 +1,141 @@
package bums.lunatic.launcher.workers
import android.annotation.SuppressLint
import android.content.Context
import androidx.work.WorkerParameters
import com.google.android.gms.location.FusedLocationProviderClient
class TelegramBotGetter : BaseGetter {
companion object {
val TAG = "TelegramBotGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
try {
//package bums.lunatic.launcher.workers
//
//import android.annotation.SuppressLint
//import android.content.Context
//import androidx.work.WorkerParameters
//import com.google.android.gms.location.FusedLocationProviderClient
//
//
//class TelegramBotGetter : BaseGetter {
// companion object {
// val TAG = "TelegramBotGetter"
// }
// constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
// }
//
//
// @SuppressLint("RestrictedApi")
// override fun realWork(): Result {
//
// try {
// val url = "https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/getUpdates"
// //"https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=71476436&text=안녕하세요."
// //7068729507
// // OkHttp 클라이언트 객체 생성
// val client = OkHttpClient.Builder().connectionPool(ConnectionPool(5,60,TimeUnit.SECONDS)).build()
//
// // GET 요청 객체 생성
// val builder: Request.Builder = Request.Builder().url(url).addHeader("Content-Type", "application/json").get()
//
// val request: Request = builder.build()
//
// BLog.LOGE("telegram before request ")
// // OkHttp 클라이언트로 GET 요청 객체 전송
// val response: Response = client.newCall(request).execute()
// if (response.isSuccessful()) {
// // 응답 받아서 처리
// val body: ResponseBody? = response.body()
// if (body != null) {
// val bodyString = body.string()
// BLog.LOGE("bodyString >>>>\n${bodyString}")
// Gson().fromJson<TelegramBotUpdate>(bodyString,TelegramBotUpdate::class.java)?.let { telegramUpdates ->
// telegramUpdates.fill()
// telegramUpdates.list.forEach {
//// if (it.message?.text?.startsWith("/") == true) {
// if((it.message?.text?.contains("where") == true) || (it.message?.text?.contains("어디") == true)) {
// BLog.LOGE("it.message?.text?.contains(\"where\") == true) >>> ${it.message?.text?.contains("where") == true}")
// BLog.LOGE("it.message?.text?.contains(\"어디\") == true) >>> ${it.message?.text?.contains("어디") == true}")
// WorkersDb.getRealm().apply {
// writeBlocking {
// if (query<TelegramData>("update_id == $0",it.update_id).find().size == 0) {
// copyToRealm(it)
// getLastLocation(context = applicationContext)
// BLog.LOGE("telegram telegramUpdates >>>> ${Gson().toJson(it)}")
// }
// }
// }
//// try {
//// val url = "https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/getUpdates"
//// //"https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=71476436&text=안녕하세요."
//// //7068729507
//// // OkHttp 클라이언트 객체 생성
//// val client = OkHttpClient.Builder().connectionPool(ConnectionPool(5,60,TimeUnit.SECONDS)).build()
////
//// // GET 요청 객체 생성
//// val builder: Request.Builder = Request.Builder().url(url).addHeader("Content-Type", "application/json").get()
////
//// val request: Request = builder.build()
////
//// BLog.LOGE("telegram before request ")
//// // OkHttp 클라이언트로 GET 요청 객체 전송
//// val response: Response = client.newCall(request).execute()
//// if (response.isSuccessful()) {
//// // 응답 받아서 처리
//// val body: ResponseBody? = response.body()
//// if (body != null) {
//// val bodyString = body.string()
//// BLog.LOGE("bodyString >>>>\n${bodyString}")
//// Gson().fromJson<TelegramBotUpdate>(bodyString,TelegramBotUpdate::class.java)?.let { telegramUpdates ->
//// telegramUpdates.fill()
//// telegramUpdates.list.forEach {
////// if (it.message?.text?.startsWith("/") == true) {
//// if((it.message?.text?.contains("where") == true) || (it.message?.text?.contains("어디") == true)) {
//// BLog.LOGE("it.message?.text?.contains(\"where\") == true) >>> ${it.message?.text?.contains("where") == true}")
//// BLog.LOGE("it.message?.text?.contains(\"어디\") == true) >>> ${it.message?.text?.contains("어디") == true}")
//// WorkersDb.getRealm().apply {
//// writeBlocking {
//// if (query<TelegramData>("update_id == $0",it.update_id).find().size == 0) {
//// copyToRealm(it)
//// getLastLocation(context = applicationContext)
//// BLog.LOGE("telegram telegramUpdates >>>> ${Gson().toJson(it)}")
//// }
//// }
//// }
////
//// }
//// }
//// }
//// }
//// } else BLog.LOGE("telegram Error Occurred")
////
//// } catch (e: java.lang.Exception) {
//// e.printStackTrace()
//// }
// } catch (e:Exception){e.printStackTrace()}
// return Result.success().apply {
//
// }
// }
// var fusedLocationProviderClient: FusedLocationProviderClient? = null
// @SuppressLint("MissingPermission")
// private fun getLastLocation(context: Context) {
//// fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context);
//// BLog.LOGE("Location getLastLocation")
//// fusedLocationProviderClient?.getLastLocation()?.addOnSuccessListener(object : OnSuccessListener<Location?> {
//// override fun onSuccess(location: Location?) {
//// if (location != null) {
//// // Log the latitude and longitude
//// BLog.LOGE("Location Latitude: " + location.getLatitude())
//// BLog.LOGE("Location Longitude: " + location.getLongitude())
////
//// // Use Geocoder to get detailed location information
//// try {
//// val geocoder = Geocoder(context, Locale.getDefault())
//// val addresses: List<Address>? = geocoder.getFromLocation(
//// location.getLatitude(),
//// location.getLongitude(),
//// 1
//// )
////
//// addresses?.first()?.let {
//// it.getAddressLine(0)?.let {
//// Executors.newSingleThreadScheduledExecutor().schedule({
//// try {
//// //////-1002450229641
//// val url =
//// "https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=83268260&text=남편의현위치는${it}"
//// //7068729507
//// // OkHttp 클라이언트 객체 생성
//// val client = OkHttpClient.Builder()
//// .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS))
//// .build()
////
//// // GET 요청 객체 생성
//// val builder: Request.Builder = Request.Builder().url(url)
//// .addHeader("Content-Type", "application/json").get()
////
//// val request: Request = builder.build()
////
//// BLog.LOGE("telegram before request ")
//// // OkHttp 클라이언트로 GET 요청 객체 전송
//// val response: Response = client.newCall(request).execute()
//// if (response.isSuccessful()) {
//// // 응답 받아서 처리
//// val body: ResponseBody? = response.body()
//// if (body != null) {
////
//// }
//// } else BLog.LOGE("telegram Error Occurred")
////
//// } catch (e: java.lang.Exception) {
//// e.printStackTrace()
//// }
//// }, 5, TimeUnit.SECONDS)
//// }
//// }
//// // Display location details on UI elements
//// // Log detailed location information
//// BLog.LOGE("Location Addresses: $addresses")
//// } catch (e: IOException) {
//// e.printStackTrace()
//// }
//// }
//// }
//// })
// }
//}
// } else BLog.LOGE("telegram Error Occurred")
//
// } catch (e: java.lang.Exception) {
// e.printStackTrace()
// }
} catch (e:Exception){e.printStackTrace()}
return Result.success().apply {
}
}
var fusedLocationProviderClient: FusedLocationProviderClient? = null
@SuppressLint("MissingPermission")
private fun getLastLocation(context: Context) {
// fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context);
// BLog.LOGE("Location getLastLocation")
// fusedLocationProviderClient?.getLastLocation()?.addOnSuccessListener(object : OnSuccessListener<Location?> {
// override fun onSuccess(location: Location?) {
// if (location != null) {
// // Log the latitude and longitude
// BLog.LOGE("Location Latitude: " + location.getLatitude())
// BLog.LOGE("Location Longitude: " + location.getLongitude())
//
// // Use Geocoder to get detailed location information
// try {
// val geocoder = Geocoder(context, Locale.getDefault())
// val addresses: List<Address>? = geocoder.getFromLocation(
// location.getLatitude(),
// location.getLongitude(),
// 1
// )
//
// addresses?.first()?.let {
// it.getAddressLine(0)?.let {
// Executors.newSingleThreadScheduledExecutor().schedule({
// try {
// //////-1002450229641
// val url =
// "https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=83268260&text=남편의현위치는${it}"
// //7068729507
// // OkHttp 클라이언트 객체 생성
// val client = OkHttpClient.Builder()
// .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS))
// .build()
//
// // GET 요청 객체 생성
// val builder: Request.Builder = Request.Builder().url(url)
// .addHeader("Content-Type", "application/json").get()
//
// val request: Request = builder.build()
//
// BLog.LOGE("telegram before request ")
// // OkHttp 클라이언트로 GET 요청 객체 전송
// val response: Response = client.newCall(request).execute()
// if (response.isSuccessful()) {
// // 응답 받아서 처리
// val body: ResponseBody? = response.body()
// if (body != null) {
//
// }
// } else BLog.LOGE("telegram Error Occurred")
//
// } catch (e: java.lang.Exception) {
// e.printStackTrace()
// }
// }, 5, TimeUnit.SECONDS)
// }
// }
// // Display location details on UI elements
// // Log detailed location information
// BLog.LOGE("Location Addresses: $addresses")
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// }
// })
}
}

View File

@ -8,14 +8,14 @@ import bums.lunatic.launcher.model.TheQoo
import bums.lunatic.launcher.model.getHref
import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.model.getT
import io.realm.kotlin.types.RealmObject
import org.jsoup.Jsoup
class TheQooGetter : BaseGetter {
class TheQooGetter(context: Context) : BaseGetter(context) {
companion object {
val TAG = "TheQooGetter"
}
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
fun parseTQoo(tq_tr : org.jsoup.nodes.Element) {
// BLog.LOGE("tq_tr >>>> ${tq_tr}")
@ -50,7 +50,7 @@ class TheQooGetter : BaseGetter {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
RssDataType.THEQOO.isOn {
try {
val testUrl2 = arrayListOf("https://theqoo.net/hot","https://theqoo.net/hot/category/512000937","https://theqoo.net/hot/category/24784","https://theqoo.net/hot/category/24788")
@ -65,9 +65,6 @@ class TheQooGetter : BaseGetter {
}
}
} catch (e:Exception){e.printStackTrace()}}
return Result.success().apply {
// BLog.LOGE("theqoo temp >>>> ${temp.size}")
WorkersDb.insertBulkData(temp)
}
return temp
}
}

View File

@ -1,9 +1,9 @@
package bums.lunatic.launcher.workers
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.apps.SimpleContact
import bums.lunatic.launcher.common.letTrue
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.AppUsageLog
import bums.lunatic.launcher.model.Astro
import bums.lunatic.launcher.model.BotCommandEentitie
import bums.lunatic.launcher.model.Condition
@ -19,6 +19,7 @@ import bums.lunatic.launcher.model.NotificationItem
import bums.lunatic.launcher.model.RssData
import bums.lunatic.launcher.model.RssDataInterface
import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.model.TelegramBotUpdate
import bums.lunatic.launcher.model.TelegramChat
import bums.lunatic.launcher.model.TelegramData
@ -45,6 +46,7 @@ import io.realm.kotlin.query.RealmQuery
import io.realm.kotlin.query.Sort
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.TypedRealmObject
import java.util.Calendar
import java.util.Locale
import java.util.regex.Pattern
import kotlin.reflect.KClass
@ -57,16 +59,90 @@ class CustMigration : AutomaticSchemaMigration {
}
object WorkersDb {
//RecentCall::class, RecentSms::class,
val clazz : Set<KClass<out BaseRealmObject>> = setOf(
RssData::class, NotificationItem::class, AppInfo::class,SimpleContact::class, RecentCall::class, RecentSms::class, CurrentPlayItem::class,
RssData::class, NotificationItem::class, AppInfo::class,SimpleContact::class, CurrentPlayItem::class,
TelegramBotUpdate::class, TelegramData::class, TelegramMessage::class, TelegramChat::class, BotCommandEentitie::class, TelegramFrom::class,
WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
LocationLog::class,
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,WidgetData::class
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
WidgetData::class,AppUsageLog::class
)
//,UserActionModel::class
// [추가] 앱/연락처 사용 시 로그 저장 (기존 updateAppUse 대신 이거 호출)
fun logAppUsage(key: String, type: String = "APP") {
val realm = getRealm()
val calendar = Calendar.getInstance()
realm.writeBlocking { // 비동기로 하려면 write { } 사용
// 1. 기존 카운트 증가 (기존 로직 유지)
// ... (AppInfo 조회 후 clickCount++ 하는 코드) ...
// 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)
})
}
}
// [핵심] 현재 시간 상황에 맞는 추천 리스트 가져오기
fun getContextualRecommendations(limit: Int = 5): List<String> {
val realm = getRealm()
val calendar = Calendar.getInstance()
val curMonth = calendar.get(Calendar.MONTH)
val curDay = calendar.get(Calendar.DAY_OF_MONTH)
val curDow = calendar.get(Calendar.DAY_OF_WEEK)
val curHour = calendar.get(Calendar.HOUR_OF_DAY)
// 최근 3개월 데이터만 조회 (너무 오래된 데이터는 노이즈가 됨)
val threeMonthsAgo = System.currentTimeMillis() - (90L * 24 * 60 * 60 * 1000)
// 쿼리: 최근 데이터만 가져와서 메모리에서 계산 (복잡한 가중치는 메모리 연산이 빠름)
val logs = realm.query<AppUsageLog>("timestamp > $0", threeMonthsAgo).find()
// 점수 계산
val scores = HashMap<String, Double>()
for (log in logs) {
var score = 1.0 // 기본 점수 (최근에 썼다는 것 자체로 의미 있음)
// 1. 시간대 매칭 (가장 중요: 아침에 쓰던 앱은 아침에 쓴다)
if (log.hour == curHour) score += 3.0
else if (log.hour == curHour - 1 || log.hour == curHour + 1) score += 1.0
// 2. 요일 매칭 (주말/평일 패턴)
if (log.dayOfWeek == curDow) score += 2.0
// 3. "매월 1일" 같은 날짜 패턴 (가중치 매우 높음)
if (log.dayOfMonth == curDay) score += 5.0
// 4. (옵션) 매년 같은 달 같은 날? (생일 등)
if (log.month == curMonth && log.dayOfMonth == curDay) score += 10.0
// 5. 최신성 가중치 (최근 기록일수록 점수 높게)
val daysAgo = (System.currentTimeMillis() - log.timestamp) / (24 * 60 * 60 * 1000)
val decay = 1.0 / (daysAgo + 1) // 오늘이면 1, 9일전이면 0.1
// 최종 점수 누적
val finalScore = score * decay
scores[log.itemKey] = (scores[log.itemKey] ?: 0.0) + finalScore
}
// 점수 높은 순으로 정렬하여 상위 N개 반환
return scores.entries
.sortedByDescending { it.value }
.take(limit)
.map { it.key }
}
val schemaVersion : Long = BuildConfig.BuildDateTime
private var pRealm : Realm? = null

View File

@ -10,21 +10,20 @@ import bums.lunatic.launcher.model.getRssData
import bums.lunatic.launcher.model.others.Youtube
import bums.lunatic.launcher.utils.RssList
import com.google.gson.Gson
import io.realm.kotlin.types.RealmObject
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
class YoutubeGetter : BaseGetter {
class YoutubeGetter(context: Context) : BaseGetter(context) {
companion object {
val YT_WORK_TAG = "YoutubeGetter"
}
var rssUrls = arrayListOf<String>()
constructor(context: Context, workerParams: WorkerParameters) : super(context, workerParams) {
}
@SuppressLint("RestrictedApi")
override fun realWork(): Result {
override fun realWork(): List<RealmObject> {
rssUrls.clear()
rssUrls.addAll(RssList.youtubeUrls)
RssDataType.YOUTUBE.isOn {
@ -32,9 +31,7 @@ class YoutubeGetter : BaseGetter {
temp.addAll(ytChannel(Jsoup.connect(url).userAgent(USAGT).get()))
}
}
return Result.success().apply {
WorkersDb.insertBulkData(temp)
}
return temp
}
fun ytChannel(doc: Document) : ArrayList<RssData> {

View File

@ -22,6 +22,7 @@
android:id="@+id/childTextview"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="5dp"
android:textColor="@color/white"
app:layout_constraintLeft_toRightOf="@id/appIconTwo"
app:layout_constraintRight_toRightOf="parent"
android:lines="2"

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="45dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/apps_bg"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/appIconTwo"
android:visibility="visible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="@dimen/forty"
android:layout_height="@dimen/forty"
android:layout_gravity="center_horizontal"
/>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/childTextview"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="5dp"
android:textColor="@color/white"
app:layout_constraintLeft_toRightOf="@id/appIconTwo"
app:layout_constraintRight_toRightOf="parent"
android:lines="2"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="0dp"
android:layout_marginBottom="@dimen/four"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/rounded_bg_top"
android:paddingTop="10dp">
<View
android:id="@+id/drag_handle"
android:layout_width="40dp"
android:layout_height="4dp"
android:background="#E0E0E0"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchInput"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_margin="@dimen/default_layout_margin"
android:layout_marginTop="20dp"
android:background="@drawable/base_bg"
android:gravity="center"
android:textColor="@color/white"
android:hint="앱 검색"
android:imeOptions="actionSearch"
android:singleLine="true"
app:layout_constraintTop_toBottomOf="@id/drag_handle"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_bg"
android:padding="@dimen/eight"
android:tint="@color/white"
android:layout_marginEnd="5dp"
app:layout_constraintBottom_toBottomOf="@id/searchInput"
app:layout_constraintRight_toRightOf="@id/searchInput"
app:layout_constraintTop_toTopOf="@id/searchInput"
app:srcCompat="@drawable/ic_refresh" />
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@id/searchInput"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp">
<TextView
android:id="@+id/appsCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:text="0 Apps"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/title_recommend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:text="앱 목록"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recAppsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:background="@android:color/transparent"
android:overScrollMode="never"
android:nestedScrollingEnabled="false"
app:layout_constraintTop_toBottomOf="@id/title_recommend"/>
<TextView
android:id="@+id/title_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:text="앱 목록"
android:textColor="@color/white"
app:layout_constraintTop_toBottomOf="@+id/recAppsList"
app:layout_constraintLeft_toLeftOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/appsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:background="@android:color/transparent"
android:overScrollMode="never"
android:nestedScrollingEnabled="false"
app:layout_constraintTop_toBottomOf="@id/title_apps"/>
<TextView
android:id="@+id/title_webs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:text="빠른 검색"
android:textColor="@color/white"
app:layout_constraintTop_toBottomOf="@id/appsList"
app:layout_constraintLeft_toLeftOf="parent"/>
<LinearLayout
android:id="@+id/quickSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp"
android:gravity="center_vertical"
app:layout_constraintTop_toBottomOf="@id/title_webs">
<bums.lunatic.launcher.view.CircleImageView
android:id="@+id/search_nmap"
style="@style/SearchIcons"
android:src="@drawable/navermap"/>
<bums.lunatic.launcher.view.CircleImageView
android:id="@+id/search_google"
style="@style/SearchIcons"
android:src="@drawable/google"/>
<bums.lunatic.launcher.view.CircleImageView
android:id="@+id/search_naver"
style="@style/SearchIcons"
android:src="@drawable/naver"/>
<bums.lunatic.launcher.view.CircleImageView
android:id="@+id/search_youtube"
style="@style/SearchIcons"
android:src="@drawable/youtube"/>
</LinearLayout>
<TextView
android:id="@+id/title_contact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:textColor="@color/white"
android:text="연락처"
app:layout_constraintTop_toBottomOf="@id/quickSearch"
app:layout_constraintLeft_toLeftOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="90dp"
android:nestedScrollingEnabled="false"
app:layout_constraintTop_toBottomOf="@id/title_contact"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,6 +10,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
android:visibility="visible"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -21,6 +22,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/number"
android:textColor="@color/white"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

View File

@ -21,9 +21,9 @@
<TextView
android:id="@+id/delete_zone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="삭제하려면 여기에 놓으세요"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="삭제"
android:textColor="#FFFFFF"
android:background="#99FF0000"
android:padding="20dp"
@ -32,7 +32,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="50dp"/>
android:layout_marginTop="10dp"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
@ -49,6 +49,7 @@
app:layout_constraintTop_toBottomOf="@id/fragment_container"
app:layout_constraintLeft_toLeftOf="parent"
android:id="@+id/back"
android:visibility="gone"
android:src="@drawable/back_vector"
tools:ignore="ContentDescription"
style="@style/CommonBottom" />
@ -57,6 +58,7 @@
app:layout_constraintTop_toBottomOf="@id/fragment_container"
app:layout_constraintLeft_toRightOf="@id/back"
android:id="@+id/reload"
android:visibility="gone"
android:src="@drawable/ic_refresh"
tools:ignore="ContentDescription"
style="@style/CommonBottom"/>
@ -68,6 +70,7 @@
app:layout_constraintLeft_toRightOf="@id/reload"
android:textColor="@color/white"
android:gravity="center"
android:visibility="gone"
android:textSize="@dimen/_12sp"
android:ellipsize="middle"
app:layout_constraintRight_toRightOf="parent"
@ -79,6 +82,7 @@
app:layout_constraintRight_toLeftOf="@id/share"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/dl_video"
android:visibility="gone"
android:src="@drawable/dl_vid"
tools:ignore="ContentDescription"
style="@style/CommonBottom"/>
@ -88,6 +92,7 @@
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginRight="60dp"
android:id="@+id/share"
android:visibility="gone"
android:foregroundTint="@color/white"
android:src="@drawable/ic_share"
tools:ignore="ContentDescription"
@ -169,5 +174,15 @@
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="close"
android:id="@+id/close"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
</bums.lunatic.launcher.view.FloatingActionMenu>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -21,4 +21,13 @@
<item name="cornerFamily">rounded</item>
<item name="cornerSize">10dp</item>
</style>
<style name="CustomBottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/CustomBottomSheetStyle</item>
</style>
<style name="CustomBottomSheetStyle" parent="Widget.Design.BottomSheet.Modal">
<item name="android:background">@android:color/transparent</item>
</style>
</resources>