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