Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/Weekly_Build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Checkout Android Keystore
uses: actions/checkout@v4
with:
repository: pppscn/keystore
repository: wudizainalilee/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
path: keystore # 仓库的根目录名
# 打包release
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.google.gson.Gson
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.MessageDedupUtils
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.SmsCommandUtils
Expand All @@ -33,7 +34,8 @@ class SmsReceiver : BroadcastReceiver() {
if (SettingUtils.enablePureClientMode) return

//过滤广播
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return
/*if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION
&& intent.action != Telephony.Sms.Intents.SMS_DELIVER_ACTION
&& intent.action != Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION
&& intent.action != Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION
Expand All @@ -59,6 +61,12 @@ class SmsReceiver : BroadcastReceiver() {
from = smsMessage.displayOriginatingAddress
msg += smsMessage.messageBody
}
}*/

val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent)
for (smsMessage in messages) {
from = smsMessage.displayOriginatingAddress
msg += smsMessage.messageBody
}
Log.d(TAG, "from = $from, msg = $msg")

Expand Down Expand Up @@ -106,6 +114,12 @@ class SmsReceiver : BroadcastReceiver() {
else -> ""
}

// 使用消息去重工具检查是否已处理过相同消息
if (MessageDedupUtils.isDuplicate(msg, from)) {
Log.d(TAG, "丢弃重复短信: $msg")
return
}

val msgInfo = MsgInfo("sms", from, msg, Date(), simInfo, simSlot, subscription)
Log.d(TAG, "msgInfo = $msgInfo")

Expand Down
270 changes: 139 additions & 131 deletions app/src/main/java/com/idormy/sms/forwarder/service/NotificationService.kt
Original file line number Diff line number Diff line change
@@ -1,132 +1,140 @@
package com.idormy.sms.forwarder.service

import android.annotation.SuppressLint
import android.content.ComponentName
import android.os.Build
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.PACKAGE_NAME
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.Worker
import com.idormy.sms.forwarder.workers.SendWorker
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xutil.display.ScreenUtils
import java.util.Date


@Suppress("PrivatePropertyName", "DEPRECATION")
class NotificationService : NotificationListenerService() {

private val TAG: String = NotificationService::class.java.simpleName

override fun onListenerConnected() {
Log.d(TAG, "onListenerConnected")
}

override fun onListenerDisconnected() {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return

//总开关
if (!SettingUtils.enableAppNotify) return

Log.d(TAG, "通知侦听器断开连接 - 请求重新绑定")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
requestRebind(ComponentName(this, NotificationListenerService::class.java))
}
}

@SuppressLint("DiscouragedPrivateApi")
override fun onNotificationPosted(sbn: StatusBarNotification?) {
try {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return

//异常通知跳过
val notification = sbn?.notification ?: return
val extras = notification.extras ?: return

//自动消除额外APP通知
SettingUtils.cancelExtraAppNotify
.takeIf { it.isNotEmpty() }
?.split("\n")
?.forEach { app ->
if (sbn.packageName == app.trim()) {
Log.d(TAG, "自动消除额外APP通知:$app")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cancelNotification(sbn.key)
} else {
cancelNotification(sbn.packageName, sbn.tag, sbn.id)
}
return@forEach
}
}


//总开关
if (!SettingUtils.enableAppNotify) return

//仅锁屏状态转发APP通知
if (SettingUtils.enableNotUserPresent && !ScreenUtils.isScreenLock()) return

val from = sbn.packageName
//自身通知跳过
if (PACKAGE_NAME == sbn.packageName) return
// 标题
val title = extras["android.title"]?.toString() ?: ""
// 通知内容
var text = extras["android.text"]?.toString() ?: ""
if (text.isEmpty() && notification.tickerText != null) {
text = notification.tickerText.toString()
}

//不处理空消息(标题跟内容都为空)
if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) return

val msgInfo = MsgInfo("app", from, text, Date(), title, -1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(TAG, "消息的UID====>" + sbn.uid)
msgInfo.uid = sbn.uid
}
//TODO:自动消除通知(临时方案,重复查询换取准确性)
if (SettingUtils.enableCancelAppNotify) {
val ruleList: List<Rule> = Core.rule.getRuleList(msgInfo.type, 1, "SIM0")
for (rule in ruleList) {
if (rule.checkMsg(msgInfo)) {
Log.d(TAG, "自动消除通知")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cancelNotification(sbn.key)
} else {
cancelNotification(sbn.packageName, sbn.tag, sbn.id)
}
break
}
}
}

val request = OneTimeWorkRequestBuilder<SendWorker>().setInputData(
workDataOf(
Worker.SEND_MSG_INFO to Gson().toJson(msgInfo),
)
).build()
WorkManager.getInstance(applicationContext).enqueue(request)

} catch (e: Exception) {
Log.e(TAG, "Parsing Notification failed: " + e.message.toString())
}

}

override fun onNotificationRemoved(sbn: StatusBarNotification?) {
Log.d(TAG, "Removed Package Name : ${sbn?.packageName}")
}

package com.idormy.sms.forwarder.service

import android.annotation.SuppressLint
import android.content.ComponentName
import android.os.Build
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.MessageDedupUtils
import com.idormy.sms.forwarder.utils.PACKAGE_NAME
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.Worker
import com.idormy.sms.forwarder.workers.SendWorker
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xutil.display.ScreenUtils
import java.util.Date


@Suppress("PrivatePropertyName", "DEPRECATION")
class NotificationService : NotificationListenerService() {

private val TAG: String = NotificationService::class.java.simpleName

override fun onListenerConnected() {
Log.d(TAG, "onListenerConnected")
}

override fun onListenerDisconnected() {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return

//总开关
if (!SettingUtils.enableAppNotify) return

Log.d(TAG, "通知侦听器断开连接 - 请求重新绑定")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
requestRebind(ComponentName(this, NotificationListenerService::class.java))
}
}

@SuppressLint("DiscouragedPrivateApi")
override fun onNotificationPosted(sbn: StatusBarNotification?) {
try {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return

//异常通知跳过
val notification = sbn?.notification ?: return
val extras = notification.extras ?: return

//自动消除额外APP通知
SettingUtils.cancelExtraAppNotify
.takeIf { it.isNotEmpty() }
?.split("\n")
?.forEach { app ->
if (sbn.packageName == app.trim()) {
Log.d(TAG, "自动消除额外APP通知:$app")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cancelNotification(sbn.key)
} else {
cancelNotification(sbn.packageName, sbn.tag, sbn.id)
}
return@forEach
}
}


//总开关
if (!SettingUtils.enableAppNotify) return

//仅锁屏状态转发APP通知
if (SettingUtils.enableNotUserPresent && !ScreenUtils.isScreenLock()) return

val from = sbn.packageName
//自身通知跳过
if (PACKAGE_NAME == sbn.packageName) return
// 标题
val title = extras["android.title"]?.toString() ?: ""
// 通知内容
var text = extras["android.text"]?.toString() ?: ""
if (text.isEmpty() && notification.tickerText != null) {
text = notification.tickerText.toString()
}

//不处理空消息(标题跟内容都为空)
if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) return

// 使用消息去重工具检查是否已处理过相同消息
// 对于SMS通知特别检查(可能已被SmsReceiver处理)
if (MessageDedupUtils.isDuplicate(text, from)) {
Log.d(TAG, "丢弃重复通知: $text")
return
}

val msgInfo = MsgInfo("app", from, text, Date(), title, -1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(TAG, "消息的UID====>" + sbn.uid)
msgInfo.uid = sbn.uid
}
//TODO:自动消除通知(临时方案,重复查询换取准确性)
if (SettingUtils.enableCancelAppNotify) {
val ruleList: List<Rule> = Core.rule.getRuleList(msgInfo.type, 1, "SIM0")
for (rule in ruleList) {
if (rule.checkMsg(msgInfo)) {
Log.d(TAG, "自动消除通知")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cancelNotification(sbn.key)
} else {
cancelNotification(sbn.packageName, sbn.tag, sbn.id)
}
break
}
}
}

val request = OneTimeWorkRequestBuilder<SendWorker>().setInputData(
workDataOf(
Worker.SEND_MSG_INFO to Gson().toJson(msgInfo),
)
).build()
WorkManager.getInstance(applicationContext).enqueue(request)

} catch (e: Exception) {
Log.e(TAG, "Parsing Notification failed: " + e.message.toString())
}

}

override fun onNotificationRemoved(sbn: StatusBarNotification?) {
Log.d(TAG, "Removed Package Name : ${sbn?.packageName}")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.idormy.sms.forwarder.utils

import java.util.concurrent.ConcurrentHashMap
import com.idormy.sms.forwarder.utils.Log

/**
* 消息重复检测工具类
* 用于防止短信被多个广播接收器或服务重复处理
*/
object MessageDedupUtils {
private val TAG: String = MessageDedupUtils::class.java.simpleName

// 消息缓存,key为消息内容+发送方,value为处理时间
private val messageCache = ConcurrentHashMap<String, Long>()

// 缓存过期时间(毫秒),10秒内的相同消息被视为重复
private const val CACHE_TTL = 10_000L

/**
* 检查消息是否已处理过
* @param content 消息内容
* @param from 发送方
* @return 如果消息已处理过返回true,否则返回false并将消息加入缓存
*/
fun isDuplicate(content: String, from: String): Boolean {
val now = System.currentTimeMillis()
val key = generateKey(content, from)

// 检查是否存在相同消息
val lastProcessed = messageCache[key]
if (lastProcessed != null && (now - lastProcessed) < CACHE_TTL) {
Log.d(TAG, "找到重复消息: $key")
return true
}

// 将消息加入缓存
messageCache[key] = now

// 清理过期缓存
cleanupExpiredCache(now)

return false
}

/**
* 生成缓存键
*/
private fun generateKey(content: String, from: String): String {
// 简单组合内容和发送方作为键
return "$from:$content"
}

/**
* 清理过期缓存
*/
private fun cleanupExpiredCache(now: Long) {
// 避免每次都清理
if (messageCache.size > 100) {
val iterator = messageCache.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (now - entry.value > CACHE_TTL) {
iterator.remove()
}
}
}
}
}