This commit is contained in:
lunaticbum 2026-01-26 15:32:30 +09:00
parent e61fa99c98
commit 16640559b3
12 changed files with 292 additions and 198 deletions

View File

@ -98,6 +98,11 @@ android {
packagingOptions.resources.excludes.add("META-INF/*") packagingOptions.resources.excludes.add("META-INF/*")
packagingOptions.resources.excludes.add("mozilla/*") packagingOptions.resources.excludes.add("mozilla/*")
packagingOptions.resources.excludes.add("META-INF/*/*") packagingOptions.resources.excludes.add("META-INF/*/*")
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"

View File

@ -23,7 +23,6 @@
<uses-permission android:name="android.permission.READ_CALL_LOG"/> <uses-permission android:name="android.permission.READ_CALL_LOG"/>
<!-- api 33+ --> <!-- api 33+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" /> <uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
@ -46,6 +45,7 @@
<uses-permission <uses-permission
android:name="android.permission.READ_SMS" android:name="android.permission.READ_SMS"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@ -58,18 +58,11 @@
<uses-permission <uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS" android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- <queries>-->
<!-- <intent>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- </intent>-->
<!-- </queries>-->
<!-- <queries>-->
<!-- <intent>-->
<!-- <action android:name="android.intent.action.SEARCH" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- </intent>-->
<!-- </queries>-->
<application <application
android:name=".LunaticLauncher" android:name=".LunaticLauncher"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
@ -82,7 +75,6 @@
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:largeHeap="true" android:largeHeap="true"
android:debuggable="false" android:debuggable="false"
android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
@ -139,29 +131,25 @@
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name=".receiver.CallReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.SmsReceiver"
android:exported="true"> <intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
</intent-filter>
</receiver>
<!-- <activity-->
<!-- android:name=".SearchAbleActivity"-->
<!-- android:theme="@style/Theme.LunarLauncher.Starting"-->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="userPortrait"-->
<!-- android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|layoutDirection|navigation"-->
<!-- android:windowSoftInputMode="adjustResize"-->
<!-- android:enabled="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.WEB_SEARCH"/>-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- </intent-filter>-->
<!-- <meta-data-->
<!-- android:name="android.app.searchable"-->
<!-- android:resource="@xml/searchable" />-->
<!-- </activity>-->
<service <service
android:name=".helpers.ForeGroundService" android:name=".helpers.ForeGroundService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".wall.MyWallpaperService" android:name=".wall.MyWallpaperService"
android:label="Bums Live Wallpaper" android:label="Bums Live Wallpaper"
@ -173,16 +161,7 @@
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/wallpaper" /> android:resource="@xml/wallpaper" />
</service> </service>
<!-- <activity-->
<!-- android:name=".apps.AppDrawer"-->
<!-- android:label="@string/lunar_settings"-->
<!-- android:launchMode="singleTask"-->
<!-- android:excludeFromRecents="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.APPLICATION_PREFERENCES" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity <activity
android:name=".home.tokiz.Settings" android:name=".home.tokiz.Settings"
@ -202,85 +181,11 @@
<action android:name="android.intent.action.APPLICATION_PREFERENCES" /> <action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <activity-->
<!-- android:name=".behavior.Behavior"-->
<!-- android:label="@string/lunar_settings"-->
<!-- android:launchMode="singleTask"-->
<!-- android:excludeFromRecents="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.APPLICATION_PREFERENCES" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<!-- <receiver android:name=".helpers.HeadsetActionButtonReceiver"--> <!-- <service-->
<!-- android:enabled="true"--> <!-- android:name=".feeds.rss.RssService"-->
<!-- android:exported="true" >--> <!-- android:permission="android.permission.BIND_JOB_SERVICE"-->
<!-- <intent-filter--> <!-- android:exported="false"/>-->
<!-- android:priority="2147483647">-->
<!-- <action android:name="android.intent.action.MEDIA_BUTTON"/>-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <service android:name=".MediaButtonService"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MEDIA_BUTTON" />-->
<!-- <action android:name="android.media.browse.MediaBrowserService" />-->
<!-- </intent-filter>-->
<!-- </service>-->
<service
android:name=".feeds.rss.RssService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<!-- <service-->
<!-- android:name=".helpers.LockService"-->
<!-- android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"-->
<!-- android:exported="false">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.accessibilityservice.AccessibilityService" />-->
<!-- </intent-filter>-->
<!-- <meta-data-->
<!-- android:name="android.accessibilityservice"-->
<!-- android:resource="@xml/lock_service" />-->
<!-- </service>-->
<!-- <receiver-->
<!-- android:name=".helpers.AdminReceiver"-->
<!-- android:label="@string/app_name"-->
<!-- android:description="@string/device_admin_description"-->
<!-- android:permission="android.permission.BIND_DEVICE_ADMIN"-->
<!-- android:exported="false">-->
<!-- <meta-data-->
<!-- android:name="android.app.device_admin"-->
<!-- android:resource="@xml/device_admin" />-->
<!-- <intent-filter>-->
<!-- <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <activity-->
<!-- android:name=".home.RssViewerActivity"-->
<!-- android:configChanges="keyboardHidden|orientation|screenSize"-->
<!-- android:hardwareAccelerated="true"-->
<!-- android:launchMode="singleTask"-->
<!-- android:exported="true"-->
<!-- android:excludeFromRecents="true"-->
<!-- android:theme="@style/FinestWebViewTheme.Fullscreen" >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:scheme="file"/>-->
<!-- <data android:scheme="content"/>-->
<!-- <data android:mimeType="text/html"/>-->
<!-- <data android:mimeType="text/plain"/>-->
<!-- <data android:mimeType="text/xml"/>-->
<!-- <data android:mimeType="application/xhtml+xml"/>-->
<!-- <data android:mimeType="application/vnd.wap.xhtml+xml"/>-->
<!-- </intent-filter>-->
<!-- </activity>-->
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
@ -305,44 +210,5 @@
<service android:name="bums.lunatic.launcher.workers.LocationUpdateService" /> <service android:name="bums.lunatic.launcher.workers.LocationUpdateService" />
<!-- <receiver android:name=".LauncherActivity$EndCallReceiver"-->
<!-- android:enabled="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.PHONE_STATE" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver android:name=".LauncherActivity$SMSReceiver"-->
<!-- android:exported="true"-->
<!-- android:enabled="true"-->
<!-- android:permission="android.permission.BROADCAST_SMS">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.provider.Telephony.SMS_RECEIVED" />-->
<!-- <action android:name="android.provider.Telephony.MMS_RECEIVED" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver-->
<!-- android:exported="true"-->
<!-- android:enabled="true"-->
<!-- android:name=".receiver.PackageEventReceiver"-->
<!-- >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.PACKAGE_REPLACED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_INSTALL"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_ADDED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_CHANGED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_FIRST_LAUNCH"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_INSTALL"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_NEEDS_VERIFICATION"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_REMOVED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_REPLACED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_RESTARTED"/>-->
<!-- <action android:name="android.intent.action.PACKAGE_VERIFIED"/>-->
<!-- <data android:scheme="package" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
</application> </application>
</manifest> </manifest>

View File

@ -10,6 +10,7 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
@ -35,6 +36,7 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 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.home.RssHome
import bums.lunatic.launcher.model.WidgetData import bums.lunatic.launcher.model.WidgetData
import bums.lunatic.launcher.receiver.NLService 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.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb import bums.lunatic.launcher.workers.WorkersDb
import bums.lunatic.launcher.workers.WorkersDb.syncSystemUsageStats
import com.google.android.material.color.DynamicColors 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.UpdatePolicy
import io.realm.kotlin.ext.query import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -135,10 +135,37 @@ open class LauncherActivity : CommonActivity() {
// 4. 롱프레스 감지 (빈 공간) // 4. 롱프레스 감지 (빈 공간)
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent) {
// 위젯 추가 메뉴 등을 띄우려면 여기서 처리 // 1. 기기 화면의 전체 높이 가져오기 (Resources 사용)
// 주의: 위젯 위에서 롱프레스하면 위젯 드래그가 먼저 작동하도록 설계해야 함 val screenHeight = resources.displayMetrics.heightPixels
// showToast("바탕화면 롱프레스: 위젯 추가")
selectWidget() // 기존에 만든 위젯 추가 함수 호출 // 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를 반환해야 다른 제스처들이 시작됨 // onDown은 true를 반환해야 다른 제스처들이 시작됨
@ -406,10 +433,10 @@ open class LauncherActivity : CommonActivity() {
restoreWidgets() restoreWidgets()
// 3. 바탕화면 롱클릭 시 위젯 추가 메뉴 띄우기 (예시) // 3. 바탕화면 롱클릭 시 위젯 추가 메뉴 띄우기 (예시)
binding.widgetContainer.setOnLongClickListener { // binding.widgetContainer.setOnLongClickListener {
selectWidget() // selectWidget()
true // true
} // }
scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() { scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean { override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
currentDragView?.let { view -> currentDragView?.let { view ->
@ -433,6 +460,41 @@ open class LauncherActivity : CommonActivity() {
return false 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) { private fun updateWidgetOptions(appWidgetId: Int, hostView: AppWidgetHostView) {
@ -732,10 +794,14 @@ open class LauncherActivity : CommonActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천) appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
} }
override fun onDestroy() { override fun onDestroy() {
smsReceiver?.let {
unregisterReceiver(it)
smsReceiver = null
}
super.onDestroy() super.onDestroy()
} }

View File

@ -283,7 +283,7 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
val pm = requireContext().packageManager val pm = requireContext().packageManager
// 1. [추천 로직] 에러가 나도 앱 목록 로딩은 진행되도록 try-catch 분리 // 1. [추천 로직] 에러가 나도 앱 목록 로딩은 진행되도록 try-catch 분리
val scoredItems = WorkersDb.getContextualRecommendations(limit = 8) val scoredItems = WorkersDb.getContextualRecommendations(limit = 18)
val unifiedList = mutableListOf<RecommendationItem>() val unifiedList = mutableListOf<RecommendationItem>()

View File

@ -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;

View File

@ -505,9 +505,7 @@ open class NeoRssActivity : CommonActivity() {
.replace(R.id.fragment_container, BookmarkPagerFragment()) .replace(R.id.fragment_container, BookmarkPagerFragment())
.commit() .commit()
} }
R.id.setting ->{
startActivity(Intent(this, SettingsActivity::class.java))
}
R.id.close ->{ R.id.close ->{
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let { supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()

View File

@ -27,4 +27,16 @@ class SimpleContact : RealmObject {
constructor() constructor()
override fun toString(): String {
return """
id : $id
name : $name
chosung : $chosung
phoneNumber : $phoneNumber
touchCount : $touchCount
lastedTouchDateTime : $lastedTouchDateTime
visibilityMode : $visibilityMode
blockRecommend : $blockRecommend
""".trimIndent()
}
} }

View File

@ -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입니다.")
}
}
}
}

View File

@ -10,6 +10,7 @@ import android.content.IntentFilter
import android.location.Geocoder import android.location.Geocoder
import android.location.Location import android.location.Location
import android.os.Build import android.os.Build
import android.provider.ContactsContract
import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import androidx.annotation.RequiresApi 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.helpers.PrefBoolean
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.KakaoPublicTransfer 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 com.google.android.gms.location.LocationServices
import java.io.IOException import java.io.IOException
import java.util.Locale import java.util.Locale
@ -69,10 +72,33 @@ class NLService : NotificationListenerService() {
stringBuffer.append(conversationTitle).append("\n") stringBuffer.append(conversationTitle).append("\n")
stringBuffer.append(summaryText).append("\n") stringBuffer.append(summaryText).append("\n")
stringBuffer.append(verificationText).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) { when (sbn.packageName) {
"com.kakao.taxi" -> { "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("돼지 택시 ") var defaultMsg: StringBuffer? = StringBuffer("돼지 택시 ")
if (stringBuffer.contains("택시") && stringBuffer.contains("탑승") && stringBuffer.contains( 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]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun makeMsgByTransferInfomation(stringBuffer : StringBuffer) { fun makeMsgByTransferInfomation(stringBuffer : StringBuffer) {
val actionIntent = Intent(this, ForeGroundService::class.java).apply { val actionIntent = Intent(this, ForeGroundService::class.java).apply {

View File

@ -62,7 +62,7 @@ class CustMigration : AutomaticSchemaMigration {
data class ScoredItem(val key: String, val type: String, val score: Double) data class ScoredItem(val key: String, val type: String, val score: Double)
enum class UsageLogType { APP, CONTACT }; enum class UsageLogType { APP, CONTACT };
enum class UsageUpdateType { JC, COUNT, DATETIME }; enum class UsageUpdateType { JC, COUNT, DATETIME,CALL, SMS };
object WorkersDb { object WorkersDb {
//RecentCall::class, RecentSms::class, //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<SimpleContact>("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<ScoredItem> { fun getContextualRecommendations(limit: Int = 18): List<ScoredItem> {
val realm = getRealm() val realm = getRealm()
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()

View File

@ -152,14 +152,6 @@
android:layout_height="20dp"/> android:layout_height="20dp"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="setting"
android:id="@+id/setting"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
<bums.lunatic.launcher.view.FloatingActionButton <bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="close" app:fab_label="close"

View File

@ -13,8 +13,8 @@ buildscript {
plugins { plugins {
val kotlinVersion: String by extra val kotlinVersion: String by extra
id ("com.android.application") version "8.6.0" apply false id ("com.android.application") version "8.10.1" apply false
id ("com.android.library") version "8.6.0" apply false id ("com.android.library") version "8.10.1" apply false
id ("io.realm.kotlin") version "2.0.0" apply false id ("io.realm.kotlin") version "2.0.0" apply false
id("org.jetbrains.kotlin.android") version kotlinVersion apply false id("org.jetbrains.kotlin.android") version kotlinVersion apply false