效果



package com.example.auth.Service
import android.accessibilityservice.AccessibilityService
import android.annotation.SuppressLint
import android.view.accessibility.AccessibilityEvent
@SuppressLint("AccessibilityPolicy")
class MyAccessibilityService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
override fun onInterrupt() {}
}package com.example.auth.sms
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.example.auth.worker.EmailWorker
class SmsObserver(
private val context: Context
) : ContentObserver(Handler(Looper.getMainLooper())) {
companion object {
var lastMsg: String? = null
var lastTime: Long = 0
}
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
Log.d("SMS_TEST", "短信数据库变化")
readLatestSms()
}
private fun readLatestSms() {
try {
val uri = Uri.parse("content://sms/inbox")
val cursor = context.contentResolver.query(
uri,
arrayOf("address", "body", "date"),
null,
null,
"date DESC LIMIT 1"
)
cursor?.use {
if (it.moveToFirst()) {
val sender = it.getString(0) ?: ""
val body = it.getString(1) ?: ""
Log.d("SMS_TEST", "最新短信: $sender -> $body")
// ======================
// 🚫 去重(关键)
// ======================
val now = System.currentTimeMillis()
if (body == lastMsg && now - lastTime < 5000) {
Log.d("SMS_TEST", "重复短信(Observer)")
return
}
lastMsg = body
if (!body.contains("验证码")){
return
}
lastTime = now
// ======================
// 🚀 发给 WorkManager
// ======================
val data = Data.Builder()
.putString("sms_content", "来自:$sender 内容:$body")
.build()
val request = OneTimeWorkRequest.Builder(EmailWorker::class.java)
.setInputData(data)
.build()
WorkManager.getInstance(context).enqueue(request)
}
}
} catch (e: Exception) {
Log.e("SMS_TEST", "读取短信失败", e)
}
}
}package com.example.auth.worker
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.auth.EmailSender
/**
* WorkManager 后台任务
* 专门负责发送邮件
*/
class EmailWorker(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
override fun doWork(): Result {
return try {
val content = inputData.getString("sms_content") ?: ""
val sp = applicationContext.getSharedPreferences("config", Context.MODE_PRIVATE)
val email = sp.getString("email", "") ?: ""
val authCode = sp.getString("authCode", "") ?: ""
if (email.isBlank() || authCode.isBlank()) {
Log.e("SMS_TEST", "邮箱或授权码为空")
return Result.failure()
}
val emailSender = EmailSender(email, authCode)
emailSender.sendEmail(
email,
"验证码通知",
"短信内容: $content"
)
Result.success()
} catch (e: Exception) {
Log.e("SMS_TEST", "Worker发送失败", e)
Result.retry()
}
}
}package com.example.auth
import android.util.Log
import java.util.Properties
import javax.mail.Authenticator
import javax.mail.Message
import javax.mail.PasswordAuthentication
import javax.mail.Session
import javax.mail.Transport
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
class EmailSender(
private val fromEmail: String,
private val authCode: String
) {
fun sendEmail(toEmail: String, subject: String, content: String) {
try {
val props = Properties().apply {
put("mail.smtp.host", "smtp.qq.com")
put("mail.smtp.port", "465")
put("mail.smtp.auth", "true")
put("mail.smtp.ssl.enable", "true")
}
val session = Session.getInstance(props, object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(fromEmail, authCode)
}
})
val message = MimeMessage(session)
message.setFrom(InternetAddress(fromEmail))
message.setRecipients(
Message.RecipientType.TO,
InternetAddress.parse(toEmail)
)
message.subject = subject
message.setText(content)
Transport.send(message)
Log.d("SMS_TEST", "邮件发送成功")
} catch (e: Exception) {
Log.e("SMS_TEST", "邮件发送失败", e)
throw e // ⚠️ 必须抛出,让 WorkManager 知道失败
}
}
}入口:
package com.example.auth
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.example.auth.sms.SmsObserver
import com.example.auth.ui.theme.AuthTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private val SMS_PERMISSION_CODE = 100
private lateinit var sp: SharedPreferences
private lateinit var smsObserver: SmsObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestSmsPermission()
// 初始化SP
sp = getSharedPreferences("config", MODE_PRIVATE)
// 注册短信监听
smsObserver = SmsObserver(this)
contentResolver.registerContentObserver(
"content://sms".toUri(),
true,
smsObserver
)
setContent {
MainUI()
}
Log.d("SMS_TEST", "App启动了")
enableEdgeToEdge()
}
// 判断读取短信权限
private fun hasSmsPermission(): Boolean {
return checkSelfPermission(android.Manifest.permission.READ_SMS) ==
PackageManager.PERMISSION_GRANTED
}
// 请求读取短信权限
private fun requestSmsPermission() {
if (!hasSmsPermission()) {
requestPermissions(
arrayOf(android.Manifest.permission.READ_SMS),
SMS_PERMISSION_CODE
)
}
}
@Composable
fun MainUI() {
var email by remember { mutableStateOf(sp.getString("email", "") ?: "") }
var authCode by remember { mutableStateOf(sp.getString("authCode", "") ?: "") }
// 👇 Snackbar 状态
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(20.dp),
verticalArrangement = Arrangement.Center
) {
Text("邮件配置", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(20.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("邮箱") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = authCode,
onValueChange = { authCode = it },
label = { Text("授权码") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
// 保存
sp.edit()
.putString("email", email)
.putString("authCode", authCode)
.apply()
// 👇 显示提示
scope.launch {
snackbarHostState.showSnackbar("保存成功 ✅")
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("保存")
}
}
}
}
override fun onDestroy() {
super.onDestroy()
contentResolver.unregisterContentObserver(smsObserver)
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AuthTheme {
Greeting("Android")
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_SMS"
tools:ignore="SmsAndCallLogPolicy" />
<uses-permission android:name="android.permission.READ_SMS"
tools:ignore="SmsAndCallLogPolicy" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Auth">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Auth">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".Service.MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true"
tools:ignore="AccessibilityPolicy">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest><?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_SMS"
tools:ignore="SmsAndCallLogPolicy" />
<uses-permission android:name="android.permission.READ_SMS"
tools:ignore="SmsAndCallLogPolicy" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Auth">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Auth">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".Service.MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true"
tools:ignore="AccessibilityPolicy">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
smsforworder