已删除2个文件
已修改10个文件
已添加18个文件
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project version="4"> |
| | | <component name="VcsDirectoryMappings"> |
| | | <mapping directory="$PROJECT_DIR$" vcs="Git" /> |
| | | </component> |
| | | </project> |
| | |
| | | <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> |
| | | <!-- 允许广播蓝牙设备信息。--> |
| | | <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> |
| | | <!-- 允许语言识别。--> |
| | | <uses-permission android:name="android.permission.RECORD_AUDIO" /> |
| | | |
| | | <!-- 去掉主题 android:theme="@style/Theme.FirstApp" android:theme="@style/Theme.AppCompat.Light.NoActionBar"--> |
| | | <application |
| | | android:name=".App" |
| | | android:allowBackup="true" |
| | |
| | | android:roundIcon="@mipmap/ic_launcher_round" |
| | | android:supportsRtl="true" |
| | | android:theme="@style/Theme.FirstApp" |
| | | android:networkSecurityConfig="@xml/network_security_config" |
| | | |
| | | tools:targetApi="31"> |
| | | <activity |
| | | android:name=".MainActivity" |
| | | android:name=".activity.LoginActivity" |
| | | android:exported="true" |
| | | android:label="@string/app_name"> |
| | | <intent-filter> |
| | |
| | | <category android:name="android.intent.category.LAUNCHER" /> |
| | | </intent-filter> |
| | | </activity> |
| | | <activity android:name=".MainActivity" /> |
| | | <activity |
| | | android:name=".activity.PhoneLoginActivity" |
| | | android:exported="false" /> |
| | | </application> |
| | | |
| | | </manifest> |
| | |
| | | import com.hjq.language.OnLanguageListener |
| | | import com.example.firstapp.core.Core |
| | | import com.example.firstapp.database.repository.CodeRepository |
| | | import com.example.firstapp.database.repository.KeywordRepository |
| | | import com.example.firstapp.database.repository.MsgRepository |
| | | import com.example.firstapp.database.service.RetrofitClient |
| | | import com.example.firstapp.receiver.CactusReceiver |
| | | import com.example.firstapp.service.BluetoothScanService |
| | | import com.example.firstapp.service.HttpServerService |
| | |
| | | val database by lazy { AppDatabase.getInstance(this) } |
| | | val msgRepository by lazy { MsgRepository(database.msgDao()) } |
| | | val codeRepository by lazy { CodeRepository(database.codeDao()) } |
| | | val keywordRepository by lazy { KeywordRepository(RetrofitClient.apiService,database.keywordDao()) } |
| | | |
| | | companion object { |
| | | const val TAG: String = "SmsForwarder" |
| | |
| | | import android.content.BroadcastReceiver |
| | | import android.content.Context |
| | | import android.content.Intent |
| | | import android.net.Uri |
| | | import androidx.lifecycle.ViewModelProvider |
| | | import androidx.recyclerview.widget.LinearLayoutManager |
| | | import androidx.recyclerview.widget.RecyclerView |
| | | import androidx.work.ExistingPeriodicWorkPolicy |
| | | import androidx.work.PeriodicWorkRequestBuilder |
| | | import androidx.work.WorkManager |
| | | import com.example.firstapp.activity.LoginActivity |
| | | import com.example.firstapp.adapter.MyAdapter |
| | | import com.example.firstapp.core.Core |
| | | import com.example.firstapp.entity.Item |
| | | import com.example.firstapp.ui.home.HomeViewModel |
| | | import com.example.firstapp.utils.Log |
| | | import com.example.firstapp.workers.KeywordUpdateWorker |
| | | import java.util.Calendar |
| | | import java.util.concurrent.TimeUnit |
| | | |
| | | class MainActivity : AppCompatActivity() { |
| | | |
| | |
| | | if (isGranted) { |
| | | // 权限授予后注册短信监听器 |
| | | registerSmsReceiver() |
| | | syncRecentSms() |
| | | } else { |
| | | // 权限拒绝,提示用户 |
| | | Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | |
| | | binding = ActivityMainBinding.inflate(layoutInflater) |
| | | setContentView(binding.root) |
| | | |
| | | setupViews() |
| | | // binding.btnLogout.setOnClickListener { |
| | | // logout() |
| | | // } |
| | | // 在此位置初始化 homeViewModel |
| | | homeViewModel = ViewModelProvider(this).get(HomeViewModel::class.java) |
| | | |
| | |
| | | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { |
| | | // 请求权限 |
| | | permissionRequest.launch(Manifest.permission.READ_SMS) |
| | | syncRecentSms() |
| | | } else { |
| | | // 权限已经授予,继续执行相关操作 |
| | | registerSmsReceiver() |
| | | syncRecentSms() |
| | | } |
| | | |
| | | |
| | | // 模拟数据 |
| | | // val items = listOf( |
| | | // Item(1, "First Item", "This is the first item description"), |
| | | // Item(2, "Second Item", "This is the second item description"), |
| | | // Item(3, "Third Item", "This is the third item description") |
| | | // ) |
| | | // |
| | | // val recyclerView = findViewById<RecyclerView>(R.id.recyclerView) |
| | | // recyclerView.layoutManager = LinearLayoutManager(this) |
| | | // recyclerView.adapter = MyAdapter(items) |
| | | |
| | | // var codeList = Core.code.getAllDesc() |
| | | val recyclerView = findViewById<RecyclerView>(R.id.recyclerView) |
| | | recyclerView.layoutManager = LinearLayoutManager(this) |
| | | // recyclerView.adapter = MyAdapter(codeList) |
| | | |
| | | // 初始化适配器 |
| | | adapter = MyAdapter() |
| | |
| | | } |
| | | |
| | | private fun registerSmsReceiver() { |
| | | // 应用启动时执行 registerSmsReceiver() |
| | | // 创建 SmsReceiver 实例 |
| | | // 注册广播接收器,开始监听短信 |
| | | // 等待新短信到达 |
| | | // 新短信到达时,系统发送广播 |
| | | // SmsReceiver 的 onReceive 方法被调用 |
| | | // 处理短信内容 |
| | | // 发送数据更新广播 |
| | | // MainActivity 接收到更新广播 |
| | | // 更新 UI |
| | | Log.d("SMS_DEBUG", "MainActivity收到数据更新广播") |
| | | smsReceiver = SmsReceiver() |
| | | val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) |
| | | registerReceiver(smsReceiver, filter) |
| | | } |
| | | |
| | | private fun setupKeywordUpdate() { |
| | | val updateRequest = PeriodicWorkRequestBuilder<KeywordUpdateWorker>( |
| | | 1, TimeUnit.HOURS, // 每小时更新一次 |
| | | 15, TimeUnit.MINUTES // 灵活时间窗口 |
| | | ).build() |
| | | |
| | | WorkManager.getInstance(this).enqueueUniquePeriodicWork( |
| | | "keyword_update", ExistingPeriodicWorkPolicy.REPLACE, updateRequest |
| | | ) |
| | | } |
| | | |
| | | private fun setupViews() { |
| | | // 获取并显示当前登录的手机号 |
| | | val phone = |
| | | getSharedPreferences("user_info", Context.MODE_PRIVATE).getString("phone", "") ?: "" |
| | | |
| | | // binding.apply { |
| | | // tvPhone.text = "当前登录手机号:$phone" |
| | | // } |
| | | } |
| | | |
| | | private fun logout() { |
| | | // 清除登录信息 |
| | | getSharedPreferences("user_info", Context.MODE_PRIVATE) |
| | | .edit() |
| | | .clear() |
| | | .apply() |
| | | |
| | | // 跳转回登录页 |
| | | startActivity(Intent(this, LoginActivity::class.java)) |
| | | finish() |
| | | } |
| | | |
| | | private fun syncRecentSms() { |
| | | try { |
| | | val calendar = Calendar.getInstance() |
| | | calendar.add(Calendar.DAY_OF_YEAR, -3) // 获取3天前的时间 |
| | | val threeDaysAgo = calendar.timeInMillis |
| | | |
| | | val cursor = contentResolver.query( |
| | | Uri.parse("content://sms/sent"), |
| | | arrayOf("address", "body", "date"), |
| | | "date >= ?", |
| | | arrayOf(threeDaysAgo.toString()), |
| | | "date DESC" |
| | | ) |
| | | |
| | | cursor?.use { |
| | | while (cursor.moveToNext()) { |
| | | val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) |
| | | val body = cursor.getString(cursor.getColumnIndexOrThrow("body")) |
| | | val date = cursor.getLong(cursor.getColumnIndexOrThrow("date")) |
| | | |
| | | // 使用正则表达式提取验证码 |
| | | val regex = "\\d{4,6}".toRegex() |
| | | val matchResult = regex.find(body) |
| | | matchResult?.let { result -> |
| | | val code = result.value |
| | | // 使用 ViewModel 保存到数据库 |
| | | println("address") |
| | | println(address) |
| | | println(code) |
| | | println(date) |
| | | // homeViewModel.saveCode(address, code, date) |
| | | } |
| | | } |
| | | } |
| | | } catch (e: Exception) { |
| | | e.printStackTrace() |
| | | Toast.makeText(this, "同步短信失败:${e.message}", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.activity |
| | | import android.os.Bundle |
| | | import android.widget.Toast |
| | | import androidx.appcompat.app.AppCompatActivity |
| | | import android.content.Intent |
| | | import com.example.firstapp.databinding.ActivityLoginBinding |
| | | |
| | | class LoginActivity : AppCompatActivity() { |
| | | private lateinit var binding: ActivityLoginBinding |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | binding = ActivityLoginBinding.inflate(layoutInflater) |
| | | setContentView(binding.root) |
| | | setupViews() |
| | | } |
| | | |
| | | private fun setupViews() { |
| | | binding.btnStartLogin.setOnClickListener { |
| | | if (binding.cbAgreement.isChecked) { |
| | | try { |
| | | val intent = Intent(this, PhoneLoginActivity::class.java) |
| | | startActivity(intent) |
| | | // 可以先不调用 finish(),确认跳转成功后再添加 |
| | | finish() |
| | | } catch (e: Exception) { |
| | | e.printStackTrace() |
| | | Toast.makeText(this, "跳转失败:${e.message}", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } else { |
| | | Toast.makeText(this, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | |
| | | binding.tvUserAgreement.setOnClickListener { |
| | | // 打开用户协议 |
| | | } |
| | | |
| | | binding.tvPrivacyPolicy.setOnClickListener { |
| | | // 打开隐私政策 |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.activity |
| | | |
| | | import android.content.Intent |
| | | import android.os.Bundle |
| | | import android.os.CountDownTimer |
| | | import android.widget.Toast |
| | | import androidx.activity.viewModels |
| | | import androidx.appcompat.app.AppCompatActivity |
| | | import com.example.firstapp.MainActivity |
| | | import com.example.firstapp.databinding.ActivityPhoneLoginBinding |
| | | import com.example.firstapp.ui.login.LoginViewModel |
| | | import com.example.firstapp.utils.Log |
| | | |
| | | class PhoneLoginActivity : AppCompatActivity() { |
| | | private lateinit var binding: ActivityPhoneLoginBinding |
| | | private val viewModel: LoginViewModel by viewModels() |
| | | private var countDownTimer: CountDownTimer? = null |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | Log.d("PhoneLoginActivity", "onCreate called") |
| | | binding = ActivityPhoneLoginBinding.inflate(layoutInflater) |
| | | setContentView(binding.root) |
| | | |
| | | setupViews() |
| | | observeViewModel() |
| | | } |
| | | |
| | | private fun setupViews() { |
| | | binding.apply { |
| | | btnBack.setOnClickListener { |
| | | finish() |
| | | } |
| | | |
| | | tvSendCode.setOnClickListener { |
| | | val phone = etPhone.text.toString() |
| | | if (phone.length == 11) { |
| | | viewModel.sendVerificationCode(phone) |
| | | startCountDown() |
| | | // 17625318565 111111 |
| | | } else { |
| | | Toast.makeText(this@PhoneLoginActivity, |
| | | "请输入正确的手机号", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | |
| | | btnLogin.setOnClickListener { |
| | | val phone = etPhone.text.toString() |
| | | val code = etCode.text.toString() |
| | | if (phone.length == 11 && code.length == 6) { |
| | | viewModel.login(phone, code) |
| | | } else { |
| | | Toast.makeText(this@PhoneLoginActivity, |
| | | "请输入完整信息", Toast.LENGTH_SHORT).show() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private fun observeViewModel() { |
| | | viewModel.loginState.observe(this) { isLoggedIn -> |
| | | if (isLoggedIn) { |
| | | startActivity(Intent(this, MainActivity::class.java)) |
| | | finishAffinity() // 结束所有之前的Activity |
| | | } |
| | | } |
| | | } |
| | | |
| | | private fun startCountDown() { |
| | | binding.tvSendCode.isEnabled = false |
| | | countDownTimer?.cancel() |
| | | countDownTimer = object : CountDownTimer(60000, 1000) { |
| | | override fun onTick(millisUntilFinished: Long) { |
| | | binding.tvSendCode.text = "${millisUntilFinished / 1000}s" |
| | | } |
| | | |
| | | override fun onFinish() { |
| | | binding.tvSendCode.isEnabled = true |
| | | binding.tvSendCode.text = "获取验证码" |
| | | } |
| | | }.start() |
| | | } |
| | | |
| | | override fun onDestroy() { |
| | | super.onDestroy() |
| | | countDownTimer?.cancel() |
| | | } |
| | | } |
| | |
| | | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { |
| | | val tvTitle: TextView = view.findViewById(R.id.tvTitle) |
| | | val tvDescription: TextView = view.findViewById(R.id.tvDescription) |
| | | val overTimeMsg: TextView = view.findViewById(R.id.overTimeMsg) |
| | | } |
| | | |
| | | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
| | |
| | | val item = getItem(position) // 使用 getItem 来获取当前位置的 item |
| | | holder.tvTitle.text = item.type // 假设 Code 类有一个 `type` 属性 |
| | | holder.tvDescription.text = item.code // 假设 Code 类有一个 `code` 属性 |
| | | var overtime = "请注意:当前取件免费截止时间是"+item.overtime+",超时会收取额外费用" |
| | | holder.overTimeMsg.text = overtime |
| | | println("打印......") |
| | | } |
| | | |
| | | // 使用 DiffUtil 来优化列表更新 |
| | |
| | | import androidx.work.Configuration |
| | | import com.example.firstapp.App |
| | | import com.example.firstapp.database.repository.CodeRepository |
| | | import com.example.firstapp.database.repository.KeywordRepository |
| | | import com.example.firstapp.database.repository.MsgRepository |
| | | |
| | | import kotlinx.coroutines.launch |
| | |
| | | |
| | | val msg: MsgRepository by lazy { (app as App).msgRepository } |
| | | val code: CodeRepository by lazy { (app as App).codeRepository } |
| | | val keyword: KeywordRepository by lazy { (app as App).keywordRepository } |
| | | |
| | | fun init(app: Application) { |
| | | this.app = app |
| | |
| | | import androidx.room.migration.Migration |
| | | import androidx.sqlite.db.SupportSQLiteDatabase |
| | | import com.example.firstapp.database.dao.CodeDao |
| | | import com.example.firstapp.database.dao.KeywordDao |
| | | import com.example.firstapp.database.dao.MsgDao |
| | | import com.example.firstapp.database.entity.Code |
| | | import com.example.firstapp.database.entity.KeywordEntity |
| | | import com.example.firstapp.database.entity.Msg |
| | | import com.example.firstapp.utils.DATABASE_NAME |
| | | import com.example.firstapp.utils.SettingUtils |
| | |
| | | |
| | | |
| | | @Database( |
| | | entities = [ Msg::class, Code::class], |
| | | entities = [ Msg::class, Code::class, KeywordEntity::class], |
| | | // views = [LogsDetail::class], |
| | | version = 19, |
| | | version = 20, |
| | | exportSchema = false |
| | | ) |
| | | @TypeConverters(ConvertersDate::class) |
| | | abstract class AppDatabase : RoomDatabase() { |
| | | abstract fun msgDao(): MsgDao |
| | | abstract fun codeDao(): CodeDao |
| | | abstract fun keywordDao(): KeywordDao |
| | | |
| | | companion object { |
| | | @Volatile |
| | |
| | | //database.execSQL("Create table Msg as Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1") |
| | | database.execSQL( |
| | | """ |
| | | CREATE TABLE "Msg" ( |
| | | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| | | "type" TEXT NOT NULL DEFAULT 'sms', |
| | | "from" TEXT NOT NULL DEFAULT '', |
| | | "content" TEXT NOT NULL DEFAULT '', |
| | | "sim_slot" INTEGER NOT NULL DEFAULT -1, |
| | | "sim_info" TEXT NOT NULL DEFAULT '', |
| | | "sub_id" INTEGER NOT NULL DEFAULT 0, |
| | | "time" INTEGER NOT NULL |
| | | ) |
| | | """.trimIndent() |
| | | CREATE TABLE "Msg" ( |
| | | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| | | "type" TEXT NOT NULL DEFAULT 'sms', |
| | | "from" TEXT NOT NULL DEFAULT '', |
| | | "content" TEXT NOT NULL DEFAULT '', |
| | | "sim_slot" INTEGER NOT NULL DEFAULT -1, |
| | | "sim_info" TEXT NOT NULL DEFAULT '', |
| | | "sub_id" INTEGER NOT NULL DEFAULT 0, |
| | | "time" INTEGER NOT NULL |
| | | ) |
| | | """.trimIndent() |
| | | ) |
| | | |
| | | database.execSQL("CREATE UNIQUE INDEX \"index_Msg_id\" ON \"Msg\" ( \"id\" ASC)") |
| | | |
| | | // 新增 KeywordEntity 表的创建逻辑 |
| | | database.execSQL(""" |
| | | CREATE TABLE IF NOT EXISTS `keywords` ( |
| | | `id` INTEGER PRIMARY KEY AUTOINCREMENT, |
| | | `keyword` TEXT NOT NULL, |
| | | `type` TEXT NOT NULL, |
| | | `isEnabled` INTEGER NOT NULL |
| | | ) |
| | | """) |
| | | |
| | | } |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.example.firstapp.database.dao |
| | | |
| | | import androidx.room.Dao |
| | | import androidx.room.Insert |
| | | import androidx.room.OnConflictStrategy |
| | | import androidx.room.Query |
| | | import androidx.room.Update |
| | | import com.example.firstapp.database.entity.KeywordConfig |
| | | import com.example.firstapp.database.entity.KeywordEntity |
| | | |
| | | @Dao |
| | | interface KeywordDao { |
| | | @Query("SELECT * FROM keywords") |
| | | fun getAllKeywords(): List<KeywordEntity> |
| | | |
| | | // @Insert(onConflict = OnConflictStrategy.REPLACE) |
| | | // suspend fun insertAll(keywords: List<KeywordEntity>) |
| | | // |
| | | // @Query("DELETE FROM keywords") |
| | | // suspend fun deleteAll() |
| | | // |
| | | // @Update |
| | | // suspend fun update(keyword: KeywordEntity) |
| | | // |
| | | // @Query("SELECT * FROM keywords WHERE isEnabled = 1") |
| | | // fun getEnabledKeywords(): List<KeywordConfig> |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.database.entity |
| | | |
| | | data class ApiResponse<T>( |
| | | val status: Int, |
| | | val msg: String, |
| | | val info: String, |
| | | val data: T |
| | | ) |
| | |
| | | val ruleId: Long, |
| | | val msgId: Long, |
| | | val code: String, |
| | | var overtime: String, |
| | | var time: Date = Date(), |
| | | ) |
对比新文件 |
| | |
| | | package com.example.firstapp.database.entity |
| | | |
| | | /** |
| | | * 关键字策略匹配类 |
| | | */ |
| | | data class KeywordConfig( |
| | | val id: Long = 0, // 自增长的 id |
| | | val type: String, |
| | | val keyword: String, |
| | | val isEnable: Boolean |
| | | ) |
对比新文件 |
| | |
| | | package com.example.firstapp.database.entity |
| | | |
| | | import androidx.room.Entity |
| | | import androidx.room.PrimaryKey |
| | | import java.util.* |
| | | |
| | | @Entity(tableName = "keywords") |
| | | data class KeywordEntity( |
| | | @PrimaryKey(autoGenerate = true) val id: Long = 0, // 自增长的 id |
| | | val keyword: String, |
| | | val type: String, |
| | | val isEnabled: Boolean |
| | | ) |
对比新文件 |
| | |
| | | package com.example.firstapp.database.repository |
| | | |
| | | import com.example.firstapp.database.dao.KeywordDao |
| | | import com.example.firstapp.database.entity.KeywordConfig |
| | | import com.example.firstapp.database.entity.KeywordEntity |
| | | import com.example.firstapp.database.service.ApiService |
| | | |
| | | class KeywordRepository( |
| | | |
| | | private val apiService: ApiService, |
| | | //本地缓存 |
| | | private val keywordDao: KeywordDao |
| | | ) { |
| | | suspend fun getKeywords(): List<KeywordEntity> { |
| | | return try { |
| | | // 从网络获取配置 |
| | | val response = apiService.getKeywords() |
| | | if (response.status == 1) { |
| | | // 保存到本地数据库作为缓存 |
| | | saveToLocal(response.data) |
| | | response.data |
| | | } else { |
| | | // 如果接口请求失败,使用本地缓存 |
| | | keywordDao.getAllKeywords() |
| | | } |
| | | } catch (e: Exception) { |
| | | e.printStackTrace() // 打印完整堆栈信息 [[3,5]] |
| | | // 或使用以下方式输出基本信息 |
| | | println("网络请求异常: ${e.message}") // 打印异常消息 [[5]] |
| | | // 网络错误时使用本地缓存 |
| | | keywordDao.getAllKeywords() |
| | | } |
| | | } |
| | | |
| | | private suspend fun saveToLocal(keywords: List<KeywordEntity>) { |
| | | true |
| | | //keywordDao.insertAll(keywords.map { it.toEntity() }) |
| | | } |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.example.firstapp.database.service |
| | | |
| | | import com.example.firstapp.database.entity.ApiResponse |
| | | import com.example.firstapp.database.entity.KeywordConfig |
| | | import com.example.firstapp.database.entity.KeywordEntity |
| | | import retrofit2.Retrofit |
| | | import retrofit2.converter.gson.GsonConverterFactory |
| | | import retrofit2.http.GET |
| | | |
| | | /** |
| | | * API调用接口 |
| | | */ |
| | | interface ApiService { |
| | | |
| | | @GET("keywords") |
| | | suspend fun getKeywords():ApiResponse<List<KeywordEntity>> //异步挂起 |
| | | } |
| | | |
| | | // 创建Retrofit实例(单例) |
| | | object RetrofitClient{ |
| | | |
| | | private const val BASE_URL ="http://47.96.225.205:9009/cloud/" |
| | | |
| | | //添加Gson解析器,用于自动将JSON响应转换为Kotlin/Java对象 |
| | | private val retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() |
| | | |
| | | //通过动态代理技术创建ApiService接口的具体实现类 |
| | | val apiService:ApiService = retrofit.create(ApiService::class.java) |
| | | |
| | | } |
| | |
| | | import android.content.BroadcastReceiver |
| | | import android.content.Context |
| | | import android.content.Intent |
| | | import android.os.Build |
| | | import android.os.Bundle |
| | | import android.provider.Telephony |
| | | import android.telephony.SmsMessage |
| | | import android.util.Log |
| | | import androidx.annotation.RequiresApi |
| | | import com.example.firstapp.core.Core |
| | | import com.example.firstapp.database.entity.Code |
| | | import com.example.firstapp.database.entity.Msg |
| | | import com.example.firstapp.database.repository.KeywordRepository |
| | | import com.example.firstapp.entity.Rule |
| | | import kotlinx.coroutines.CoroutineScope |
| | | import kotlinx.coroutines.Dispatchers |
| | | import kotlinx.coroutines.launch |
| | | import java.time.LocalDateTime |
| | | import java.time.format.DateTimeFormatter |
| | | |
| | | |
| | | class SmsReceiver : BroadcastReceiver() { |
| | | |
| | | |
| | | @RequiresApi(Build.VERSION_CODES.O) |
| | | override fun onReceive(context: Context, intent: Intent) { |
| | | |
| | | // 检查广播的 Action 是否为短信接收 |
| | | if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION == intent.action) { |
| | | // 获取短信内容 |
| | |
| | | Rule("快递","菜鸟驿站","\\d{1,2}-\\d{1,2}-\\d{4}") |
| | | ) |
| | | |
| | | CoroutineScope(Dispatchers.IO).launch { |
| | | Log.d("SmsReceiver", "CoroutineScope started") |
| | | // 获取最新的关键词配置 |
| | | val keywords = Core.keyword.getKeywords() |
| | | Log.d("keywords", keywords.toString()) |
| | | println(keywords) |
| | | // 保存匹配的短信 |
| | | //saveMessage(content) |
| | | } |
| | | |
| | | |
| | | // kotlin 怎么创建一个类 |
| | | for (rule in ruleList) { |
| | | val code = rule.extractCodeFromMessage(messageBody.toString()) |
| | | |
| | | if (code!==null) { |
| | | Log.d("SmsReceiver", "Received SMS code: ${code}") |
| | | |
| | | |
| | | // 获取当前时间 |
| | | val currentTime = LocalDateTime.now() |
| | | // 加2小时 |
| | | val futureTime = currentTime.plusHours(2) |
| | | // 定义时间格式 |
| | | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") |
| | | // 转换为字符串 |
| | | val overtime = futureTime.format(formatter) |
| | | // 封装成一个Code对象,并保存在数据库中 |
| | | val code = Code(0, rule.type,1, rule.content,1, 1, msgId, code) |
| | | val code = Code(0, rule.type,1, rule.content,1, 1, msgId, code, overtime) |
| | | Core.code.insert(code) |
| | | Log.d("SMS_DEBUG", "新短信已保存到数据库") |
| | | // 发送广播通知数据已更新 |
| | | //"com.example.firstapp.DATA_UPDATED" 是一个自定义的广播 Action,相当于一个标识符或者说是一个频道名称。这个名称是我们自己定义的,通常使用应用的包名作为前缀,以避免与其他应用的广播冲突。 |
| | | val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") |
| | | context.sendBroadcast(updateIntent) |
| | | Log.d("SMS_DEBUG", "发送数据更新广播") |
| | | }else{ |
| | | Log.d("SmsReceiver", "Received SMS code: 没有匹配到内容") |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.ui.login |
| | | |
| | | import android.app.Application |
| | | import android.content.Context |
| | | import androidx.lifecycle.LiveData |
| | | import androidx.lifecycle.MutableLiveData |
| | | import androidx.lifecycle.viewModelScope |
| | | import kotlinx.coroutines.delay |
| | | import kotlinx.coroutines.launch |
| | | import androidx.lifecycle.AndroidViewModel |
| | | |
| | | |
| | | class LoginViewModel(application: Application) : AndroidViewModel(application) { |
| | | private val _loginState = MutableLiveData<Boolean>() |
| | | val loginState: LiveData<Boolean> = _loginState |
| | | |
| | | fun sendVerificationCode(phone: String) { |
| | | viewModelScope.launch { |
| | | // 这里实现发送验证码的逻辑 |
| | | // 模拟网络请求 |
| | | delay(1000) |
| | | // 实际应用中需要调用后端API |
| | | } |
| | | } |
| | | |
| | | fun login(phone: String, code: String) { |
| | | viewModelScope.launch { |
| | | // 模拟登录请求 |
| | | delay(1000) |
| | | // 保存登录状态和手机号 |
| | | saveLoginInfo(phone) |
| | | _loginState.value = true |
| | | } |
| | | } |
| | | |
| | | private fun saveLoginInfo(phone: String) { |
| | | getApplication<Application>().getSharedPreferences( |
| | | "user_info", Context.MODE_PRIVATE |
| | | ).edit().apply { |
| | | putBoolean("is_logged_in", true) |
| | | putString("phone", phone) |
| | | apply() |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.workers |
| | | |
| | | import android.content.Context |
| | | import androidx.work.CoroutineWorker |
| | | import androidx.work.WorkerParameters |
| | | import com.example.firstapp.database.dao.KeywordDao |
| | | import com.example.firstapp.database.repository.KeywordRepository |
| | | import com.example.firstapp.database.service.RetrofitClient |
| | | |
| | | class KeywordUpdateWorker( |
| | | context: Context, |
| | | params: WorkerParameters, |
| | | private val keywordDao: KeywordDao // 注入 KeywordDao |
| | | ) : CoroutineWorker(context, params) { |
| | | |
| | | override suspend fun doWork(): Result { |
| | | // 另外一种实例化的方式 |
| | | // val database = AppDatabase.getInstance(applicationContext) // 获取数据库实例 |
| | | // val keywordDao = database.keywordDao() // 获取 KeywordDao 实例 |
| | | // val repository = KeywordRepository(RetrofitClient.apiService, keywordDao) // 传递 keywordDao |
| | | |
| | | val repository = KeywordRepository(RetrofitClient.apiService,keywordDao) |
| | | return try { |
| | | // 更新配置 |
| | | repository.getKeywords() |
| | | Result.success() |
| | | } catch (e: Exception) { |
| | | Result.retry() |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | xmlns:app="http://schemas.android.com/apk/res-auto" |
| | | |
| | | android:layout_width="match_parent" |
| | | android:layout_height="match_parent" |
| | | android:orientation="vertical" |
| | | android:padding="24dp"> |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="欢迎来到" |
| | | android:textSize="24sp" |
| | | android:textColor="#333333" /> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="智信管家!" |
| | | android:textSize="28sp" |
| | | android:textStyle="bold" |
| | | android:textColor="#333333" /> |
| | | |
| | | <ImageView |
| | | android:id="@+id/ivLogo" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="24dp" |
| | | android:layout_marginBottom="24dp" |
| | | android:layout_weight="0" |
| | | android:scaleType="centerCrop" |
| | | android:src="@drawable/login_robot" /> |
| | | |
| | | <Button |
| | | android:id="@+id/btnStartLogin" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="16dp" |
| | | android:text="手机号登录/注册" |
| | | android:textColor="#FFFFFF" |
| | | android:textSize="16sp" |
| | | android:backgroundTint="@color/cardview_dark_background" |
| | | app:cornerRadius="14dp" /> |
| | | |
| | | |
| | | <LinearLayout |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_gravity="center" |
| | | android:layout_marginTop="16dp" |
| | | android:orientation="horizontal"> |
| | | |
| | | <CheckBox |
| | | android:id="@+id/cbAgreement" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:checked="true" /> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="我已阅读并同意" |
| | | android:textSize="14sp" /> |
| | | |
| | | <TextView |
| | | android:id="@+id/tvUserAgreement" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="用户协议" |
| | | android:textColor="#2196F3" |
| | | android:textSize="14sp" /> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text=" & " |
| | | android:textSize="14sp" /> |
| | | |
| | | <TextView |
| | | android:id="@+id/tvPrivacyPolicy" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="隐私政策" |
| | | android:textColor="#2196F3" |
| | | android:textSize="14sp" /> |
| | | </LinearLayout> |
| | | |
| | | </LinearLayout> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | xmlns:app="http://schemas.android.com/apk/res-auto" |
| | | |
| | | android:layout_width="match_parent" |
| | | android:layout_height="match_parent" |
| | | android:orientation="vertical" |
| | | android:padding="24dp"> |
| | | |
| | | <ImageButton |
| | | android:id="@+id/btnBack" |
| | | android:layout_width="48dp" |
| | | android:layout_height="48dp" |
| | | android:background="?attr/selectableItemBackgroundBorderless" |
| | | android:src="@drawable/left_forward2" |
| | | android:padding="12dp" /> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="24dp" |
| | | android:text="手机登录" |
| | | android:textSize="24sp" |
| | | android:textStyle="bold" |
| | | android:textColor="#333333" /> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="32dp" |
| | | android:orientation="vertical"> |
| | | |
| | | <EditText |
| | | android:id="@+id/etPhone" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:background="@null" |
| | | android:hint="请输入手机号" |
| | | android:text="177625318565" |
| | | android:inputType="phone" |
| | | android:maxLength="11" |
| | | android:textSize="16sp" |
| | | android:padding="12dp"/> |
| | | |
| | | <View |
| | | android:layout_width="match_parent" |
| | | android:layout_height="1dp" |
| | | android:background="#EEEEEE" /> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal" |
| | | android:layout_marginTop="16dp"> |
| | | |
| | | <EditText |
| | | android:id="@+id/etCode" |
| | | android:layout_width="0dp" |
| | | android:layout_height="wrap_content" |
| | | android:layout_weight="1" |
| | | android:background="@null" |
| | | android:hint="请输入验证码" |
| | | android:text="123456" |
| | | android:inputType="number" |
| | | android:maxLength="6" |
| | | android:textSize="16sp" |
| | | android:padding="12dp"/> |
| | | |
| | | <TextView |
| | | android:id="@+id/tvSendCode" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:padding="12dp" |
| | | android:text="获取验证码" |
| | | android:textColor="#2196F3"/> |
| | | </LinearLayout> |
| | | |
| | | <View |
| | | android:layout_width="match_parent" |
| | | android:layout_height="1dp" |
| | | android:background="#EEEEEE" /> |
| | | </LinearLayout> |
| | | |
| | | <Button |
| | | android:id="@+id/btnLogin" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="32dp" |
| | | android:backgroundTint="@color/cardview_dark_background" |
| | | android:text="登录" |
| | | android:textColor="#FFFFFF" |
| | | android:textSize="16sp" |
| | | android:padding="12dp" |
| | | app:cornerRadius="14dp"/> |
| | | </LinearLayout> |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | xmlns:app="http://schemas.android.com/apk/res-auto" |
| | | xmlns:tools="http://schemas.android.com/tools" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="match_parent" |
| | | tools:context=".ui.notifications.NotificationsFragment"> |
| | | android:orientation="vertical"> |
| | | |
| | | <!-- 标题栏 --> |
| | | <TextView |
| | | android:id="@+id/text_notifications" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginStart="8dp" |
| | | android:layout_marginTop="8dp" |
| | | android:layout_marginEnd="8dp" |
| | | android:textAlignment="center" |
| | | android:textSize="20sp" |
| | | app:layout_constraintBottom_toBottomOf="parent" |
| | | app:layout_constraintEnd_toEndOf="parent" |
| | | app:layout_constraintStart_toStartOf="parent" |
| | | app:layout_constraintTop_toTopOf="parent" /> |
| | | </androidx.constraintlayout.widget.ConstraintLayout> |
| | | android:background="#FFE4C4" |
| | | android:gravity="center" |
| | | android:padding="16dp" |
| | | android:text="终身会员" |
| | | android:textSize="18sp" /> |
| | | |
| | | <!-- 功能区标题 --> |
| | | <TextView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="8dp" |
| | | android:text="功能" |
| | | android:textSize="14sp" /> |
| | | |
| | | <!-- 设置选项 --> |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="vertical"> |
| | | |
| | | <!-- 设置提醒 --> |
| | | <RelativeLayout |
| | | android:id="@+id/settings_reminder" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="设置提醒" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 取录与反馈 --> |
| | | <TextView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="8dp" |
| | | android:text="取录与反馈" |
| | | android:textSize="14sp" /> |
| | | |
| | | <!-- 关于小红书 --> |
| | | <RelativeLayout |
| | | android:id="@+id/about_app" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="关于小红书" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 邮件联系 --> |
| | | <RelativeLayout |
| | | android:id="@+id/email_contact" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="邮件联系" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 意见与反馈 --> |
| | | <RelativeLayout |
| | | android:id="@+id/feedback" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="意见与反馈" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 分享给好友 --> |
| | | <RelativeLayout |
| | | android:id="@+id/share_to_friends" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="分享给好友" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 其他区域标题 --> |
| | | <TextView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="8dp" |
| | | android:text="其他" |
| | | android:textSize="14sp" /> |
| | | |
| | | <!-- 隐私协议 --> |
| | | <RelativeLayout |
| | | android:id="@+id/privacy_policy" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="隐私协议" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | <!-- 如何使用 --> |
| | | <RelativeLayout |
| | | android:id="@+id/how_to_use" |
| | | style="@style/SettingsItem"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="如何使用" /> |
| | | |
| | | <ImageView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@android:drawable/ic_menu_more" /> |
| | | </RelativeLayout> |
| | | |
| | | </LinearLayout> |
| | | |
| | | </LinearLayout> |
| | |
| | | android:orientation="vertical" |
| | | android:padding="16dp"> |
| | | |
| | | |
| | | |
| | | <TextView |
| | | android:id="@+id/tvTitle" |
| | | android:layout_width="match_parent" |
| | |
| | | android:layout_height="wrap_content" |
| | | android:textSize="14sp" |
| | | android:layout_marginTop="4dp"/> |
| | | |
| | | <TextView |
| | | android:id="@+id/overTimeMsg" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:layout_marginTop="4dp" |
| | | android:textSize="14sp" /> |
| | | </LinearLayout> |
对比新文件 |
| | |
| | | <style name="SettingsItem"> |
| | | <item name="android:layout_width">match_parent</item> |
| | | <item name="android:layout_height">wrap_content</item> |
| | | <item name="android:padding">16dp</item> |
| | | <item name="android:background">?android:attr/selectableItemBackground</item> |
| | | <item name="android:clickable">true</item> |
| | | <item name="android:focusable">true</item> |
| | | </style> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <network-security-config> |
| | | <domain-config cleartextTrafficPermitted="true"> |
| | | <domain includeSubdomains="true">47.96.225.205</domain> |
| | | <!-- 可添加其他域名或IP(如192.168.0.101) --> |
| | | </domain-config> |
| | | </network-security-config> |