diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 96ce70c0..2bc8d4fd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -98,6 +98,11 @@ android {
packagingOptions.resources.excludes.add("META-INF/*")
packagingOptions.resources.excludes.add("mozilla/*")
packagingOptions.resources.excludes.add("META-INF/*/*")
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
kotlinOptions {
jvmTarget = "1.8"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 57a1bcae..ce96f5ee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,7 +23,6 @@
-
@@ -46,6 +45,7 @@
+
@@ -58,18 +58,11 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
index 6db8e99f..4fa3aac1 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt
@@ -10,6 +10,7 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
@@ -35,6 +36,7 @@ import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -54,13 +56,11 @@ import bums.lunatic.launcher.home.NeoRssActivity
import bums.lunatic.launcher.home.RssHome
import bums.lunatic.launcher.model.WidgetData
import bums.lunatic.launcher.receiver.NLService
+import bums.lunatic.launcher.receiver.SmsReceiver
+import bums.lunatic.launcher.settings.SettingsActivity
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
-import bums.lunatic.launcher.workers.WorkersDb.syncSystemUsageStats
import com.google.android.material.color.DynamicColors
-import com.yausername.ffmpeg.FFmpeg
-import com.yausername.youtubedl_android.YoutubeDL
-import com.yausername.youtubedl_android.YoutubeDLException
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope
@@ -135,10 +135,37 @@ open class LauncherActivity : CommonActivity() {
// 4. 롱프레스 감지 (빈 공간)
override fun onLongPress(e: MotionEvent) {
- // 위젯 추가 메뉴 등을 띄우려면 여기서 처리
- // 주의: 위젯 위에서 롱프레스하면 위젯 드래그가 먼저 작동하도록 설계해야 함
-// showToast("바탕화면 롱프레스: 위젯 추가")
- selectWidget() // 기존에 만든 위젯 추가 함수 호출
+ // 1. 기기 화면의 전체 높이 가져오기 (Resources 사용)
+ val screenHeight = resources.displayMetrics.heightPixels
+
+ // 2. 롱프레스가 발생한 절대 Y 좌표
+ val touchY = e.rawY
+
+ // 3. 높이에 따른 분기 처리
+ when {
+ touchY < screenHeight / 3 -> {
+ // 상단 롱프레스 (0 ~ 33%)
+ handleTopLongPress()
+ }
+ touchY < (screenHeight / 3) * 2 -> {
+ // 중단 롱프레스 (33% ~ 66%)
+ selectWidget() // 기존 함수
+ }
+ else -> {
+ // 하단 롱프레스 (66% ~ 100%)
+ handleBottomLongPress()
+ }
+ }
+ }
+
+ // 각 구간별로 실행할 함수들 (예시)
+ private fun handleTopLongPress() {
+ // 상단 전용 기능
+ }
+
+ private fun handleBottomLongPress() {
+ // 하단 전용 기능
+ startActivity(Intent(this@LauncherActivity, SettingsActivity::class.java))
}
// onDown은 true를 반환해야 다른 제스처들이 시작됨
@@ -406,10 +433,10 @@ open class LauncherActivity : CommonActivity() {
restoreWidgets()
// 3. 바탕화면 롱클릭 시 위젯 추가 메뉴 띄우기 (예시)
- binding.widgetContainer.setOnLongClickListener {
- selectWidget()
- true
- }
+// binding.widgetContainer.setOnLongClickListener {
+// selectWidget()
+// true
+// }
scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
currentDragView?.let { view ->
@@ -433,6 +460,41 @@ open class LauncherActivity : CommonActivity() {
return false
}
})
+
+ requestSmsPermissionLauncher.launch(arrayOf(
+ android.Manifest.permission.RECEIVE_SMS,
+ android.Manifest.permission.READ_SMS
+ ))
+ }
+
+ private var smsReceiver: SmsReceiver? = null
+
+ // 권한 요청 결과 처리기
+ private val requestSmsPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ val granted = permissions[android.Manifest.permission.RECEIVE_SMS] ?: false
+ if (granted) {
+ Blog.LOGE("SMS 권한 승인됨 -> 리시버 동적 등록 실행")
+ registerSmsDynamicReceiver()
+ }
+ }
+
+ private fun registerSmsDynamicReceiver() {
+ if (smsReceiver == null) {
+ smsReceiver = SmsReceiver()
+ val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED").apply {
+ priority = 2147483647 // 시스템 최우선 순위
+ }
+
+ // Android 14 이상(안드 16 포함) 필수 플래그: RECEIVER_EXPORTED
+ // 외부 앱(시스템 타워)으로부터 브로드캐스트를 받으려면 필수입니다.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED)
+ } else {
+ registerReceiver(smsReceiver, filter)
+ }
+ }
}
private fun updateWidgetOptions(appWidgetId: Int, hostView: AppWidgetHostView) {
@@ -732,10 +794,14 @@ open class LauncherActivity : CommonActivity() {
override fun onStop() {
super.onStop()
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
+
}
override fun onDestroy() {
-
+ smsReceiver?.let {
+ unregisterReceiver(it)
+ smsReceiver = null
+ }
super.onDestroy()
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
index d33236f4..45d18774 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/apps/AppDrawerBottomSheet.kt
@@ -283,7 +283,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val pm = requireContext().packageManager
// 1. [추천 로직] 에러가 나도 앱 목록 로딩은 진행되도록 try-catch 분리
- val scoredItems = WorkersDb.getContextualRecommendations(limit = 8)
+ val scoredItems = WorkersDb.getContextualRecommendations(limit = 18)
val unifiedList = mutableListOf()
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/apps/test.java b/app/src/main/kotlin/bums/lunatic/launcher/apps/test.java
deleted file mode 100644
index 1e4b48c6..00000000
--- a/app/src/main/kotlin/bums/lunatic/launcher/apps/test.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package bums.lunatic.launcher.apps;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Base64; // Java 기본 Base64 사용
-import javax.crypto.Cipher;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
index 391a7242..92c5cacd 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
@@ -505,9 +505,7 @@ open class NeoRssActivity : CommonActivity() {
.replace(R.id.fragment_container, BookmarkPagerFragment())
.commit()
}
- R.id.setting ->{
- startActivity(Intent(this, SettingsActivity::class.java))
- }
+
R.id.close ->{
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
supportFragmentManager.beginTransaction()
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/model/SimpleContact.kt b/app/src/main/kotlin/bums/lunatic/launcher/model/SimpleContact.kt
index ee8b0383..9fbad78e 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/model/SimpleContact.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/model/SimpleContact.kt
@@ -27,4 +27,16 @@ class SimpleContact : RealmObject {
constructor()
+ override fun toString(): String {
+ return """
+ id : $id
+ name : $name
+ chosung : $chosung
+ phoneNumber : $phoneNumber
+ touchCount : $touchCount
+ lastedTouchDateTime : $lastedTouchDateTime
+ visibilityMode : $visibilityMode
+ blockRecommend : $blockRecommend
+ """.trimIndent()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/receiver/ContactReceiver.kt b/app/src/main/kotlin/bums/lunatic/launcher/receiver/ContactReceiver.kt
new file mode 100644
index 00000000..dc887890
--- /dev/null
+++ b/app/src/main/kotlin/bums/lunatic/launcher/receiver/ContactReceiver.kt
@@ -0,0 +1,67 @@
+package bums.lunatic.launcher.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.telephony.SmsMessage
+import android.telephony.TelephonyManager
+import android.widget.Toast
+import bums.lunatic.launcher.utils.Blog
+import bums.lunatic.launcher.workers.UsageUpdateType
+import bums.lunatic.launcher.workers.WorkersDb
+
+// 1. 전화 감지 리시버
+class CallReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == TelephonyManager.ACTION_PHONE_STATE_CHANGED) {
+ val state = intent.getStringExtra(TelephonyManager.EXTRA_STATE)
+ val number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)
+
+ // 전화 벨이 울릴 때 (수신)
+ if (state == TelephonyManager.EXTRA_STATE_RINGING && number != null) {
+ WorkersDb.logContactInteraction(number, UsageUpdateType.CALL)
+ }
+ }
+ }
+}
+
+// 2. 문자 감지 리시버
+class SmsReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ // 1. 액션 수신 확인 로그
+ Blog.LOGE("SMS_RECEIVED 브로드캐스트 수신됨: ${intent.action}")
+ Toast.makeText(context, "문자 수신 신호 감지!", Toast.LENGTH_SHORT).show()
+ Blog.LOGE("Action: ${intent.action}")
+
+ if (intent.action == "android.provider.Telephony.SMS_RECEIVED") {
+ val bundle = intent.extras
+ val pdus = bundle?.get("pdus") as? Array<*>
+ // format이 null일 경우를 대비해 기본값 "3gpp"(GSM) 또는 "3gpp2"(CDMA) 처리
+ val format = bundle?.getString("format") ?: "3gpp"
+
+ if (pdus != null) {
+ for (pdu in pdus) {
+ try {
+ val sms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ SmsMessage.createFromPdu(pdu as ByteArray, format)
+ } else {
+ SmsMessage.createFromPdu(pdu as ByteArray)
+ }
+
+ val senderNumber = sms.originatingAddress
+ Blog.LOGE("수신된 번호: $senderNumber")
+
+ if (senderNumber != null) {
+ WorkersDb.logContactInteraction(senderNumber, UsageUpdateType.SMS)
+ }
+ } catch (e: Exception) {
+ Blog.LOGE("SMS 파싱 오류: ${e.message}")
+ }
+ }
+ } else {
+ Blog.LOGE("SMS 데이터(pdus)가 null입니다.")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
index eff458b3..8949f41d 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt
@@ -10,6 +10,7 @@ import android.content.IntentFilter
import android.location.Geocoder
import android.location.Location
import android.os.Build
+import android.provider.ContactsContract
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.annotation.RequiresApi
@@ -21,6 +22,8 @@ import bums.lunatic.launcher.helpers.ForeGroundService.Companion.EXTRA_MSGKEY
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.KakaoPublicTransfer
+import bums.lunatic.launcher.workers.UsageUpdateType
+import bums.lunatic.launcher.workers.WorkersDb
import com.google.android.gms.location.LocationServices
import java.io.IOException
import java.util.Locale
@@ -69,10 +72,33 @@ class NLService : NotificationListenerService() {
stringBuffer.append(conversationTitle).append("\n")
stringBuffer.append(summaryText).append("\n")
stringBuffer.append(verificationText).append("\n")
-// Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
- mHourlyLogWriter?.writeLog("${sbn.packageName}\n${stringBuffer.toString()}")
+
+ if (sbn.packageName == "com.samsung.android.messaging" || sbn.packageName == "com.google.android.apps.messaging") {
+ val extras = sbn.notification.extras
+ val title = extras.getString(Notification.EXTRA_TITLE) ?: return
+ val cleanTitle = sanitizeIdentifier(title)
+ if (cleanTitle.isEmpty()) return
+ // 1. 제목이 숫자인지 확인 (정규식: 숫자, +, - 만 포함된 경우)
+ val isNumber = cleanTitle.matches(Regex("^[0-9+\\- ]+$"))
+
+ if (isNumber) {
+ // 번호가 바로 왔을 때
+ WorkersDb.logContactInteraction(cleanTitle, UsageUpdateType.SMS)
+ } else {
+ // 이름이 왔을 때 -> 주소록에서 번호 조회
+ val foundNumber = getPhoneNumberByName(applicationContext, cleanTitle)
+ if (foundNumber != null) {
+ Blog.LOGE("이름($title)으로 번호($foundNumber) 조회 성공")
+ WorkersDb.logContactInteraction(foundNumber, UsageUpdateType.SMS)
+ } else {
+ // 주소록에도 없는 이름일 경우 (이름 자체로 로그를 남기려면 WorkersDb 수정 필요)
+ Blog.LOGE("주소록에서 찾을 수 없는 이름: $title")
+ }
+ }
+ }
when (sbn.packageName) {
"com.kakao.taxi" -> {
+ Blog.LOGE("packageName ${sbn.packageName} :::: title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}")
var defaultMsg: StringBuffer? = StringBuffer("돼지 택시 ")
if (stringBuffer.contains("택시") && stringBuffer.contains("탑승") && stringBuffer.contains(
"완료"
@@ -128,6 +154,35 @@ class NLService : NotificationListenerService() {
}
}
+ fun sanitizeIdentifier(input: String): String {
+ // 1. 한글(가-힣), 초성(ㄱ-ㅎ), 숫자(0-9), 영문(a-zA-Z)만 남기고 모두 제거
+ val regex = Regex("[^가-힣ㄱ-ㅎ0-9a-zA-Z]")
+ val cleaned = input.replace(regex, "").trim()
+
+ Blog.LOGE("정제 전: [$input] -> 정제 후: [$cleaned]")
+ return cleaned
+ }
+
+ fun getPhoneNumberByName(context: Context, name: String): String? {
+ val contentResolver = context.contentResolver
+
+ // 이름의 일부라도 포함되어 있는지 검색 (%name%)
+ val cursor = contentResolver.query(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
+ "${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} LIKE ?",
+ arrayOf("%$name%"),
+ null
+ )
+
+ cursor?.use {
+ if (it.moveToFirst()) {
+ return it.getString(0).replace(Regex("[^0-9]"), "")
+ }
+ }
+ return null
+ }
+
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun makeMsgByTransferInfomation(stringBuffer : StringBuffer) {
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
index 1d520f07..e0219a17 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
@@ -62,7 +62,7 @@ class CustMigration : AutomaticSchemaMigration {
data class ScoredItem(val key: String, val type: String, val score: Double)
enum class UsageLogType { APP, CONTACT };
-enum class UsageUpdateType { JC, COUNT, DATETIME };
+enum class UsageUpdateType { JC, COUNT, DATETIME,CALL, SMS };
object WorkersDb {
//RecentCall::class, RecentSms::class,
@@ -141,8 +141,51 @@ object WorkersDb {
}
}
+ fun logContactInteraction(phoneNumber: String, updateType: UsageUpdateType) {
+ val realm = getRealm()
+ val calendar = Calendar.getInstance()
+
+ // 전화번호에서 하이픈 제거 등 포맷 정리 (DB 저장 형식에 맞춤)
+ val normalizedNumber = phoneNumber.replace("-", "").replace(" ", "")
+
+ realm.writeBlocking {
+ // 1. 전화번호로 해당 연락처 찾기 (SimpleContact에 phone 필드가 있다고 가정)
+ val contact = query("phoneNumber == $0", normalizedNumber).first().find()
+
+ if (contact != null) {
+ val key = contact.id // 추천 시스템에서 사용하는 키값
+ Blog.LOGE("contact >>> ${contact}")
+ when (updateType) {
+ UsageUpdateType.CALL -> {
+ contact.touchCount += 20 // 통화는 가중치를 더 높게 설정
+ contact.lastedTouchDateTime = System.currentTimeMillis()
+ }
+ UsageUpdateType.SMS -> {
+ contact.touchCount += 10 // 문자는 중간 정도
+ contact.lastedTouchDateTime = System.currentTimeMillis()
+ }
+ else -> {
+ contact.touchCount += 1
+ }
+ }
+ key?.let { key ->
+ // 2. 추천 시스템이 사용하는 AppUsageLog에도 기록 추가
+ copyToRealm(AppUsageLog().apply {
+ itemKey = key
+ itemType = UsageLogType.CONTACT.name
+ timestamp = System.currentTimeMillis()
+ month = calendar.get(Calendar.MONTH)
+ dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
+ dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
+ hour = calendar.get(Calendar.HOUR_OF_DAY)
+ })
+ }
+ }
+ }
+ }
+
// [핵심] 현재 시간 상황에 맞는 추천 리스트 가져오기
- fun getContextualRecommendations(limit: Int = 10): List {
+ fun getContextualRecommendations(limit: Int = 18): List {
val realm = getRealm()
val calendar = Calendar.getInstance()
diff --git a/app/src/main/res/layout/rss_activity.xml b/app/src/main/res/layout/rss_activity.xml
index f58a3180..54f03f9a 100644
--- a/app/src/main/res/layout/rss_activity.xml
+++ b/app/src/main/res/layout/rss_activity.xml
@@ -152,14 +152,6 @@
android:layout_height="20dp"/>
-