Merge remote-tracking branch 'origin/master'
# Conflicts:
# app/src/main/AndroidManifest.xml
| | |
| | | android:theme="@style/Theme.ContentDetail" /> |
| | | <activity |
| | | android:name=".ui.profile.EditProfileActivity" |
| | | android:exported="false" /> |
| | | android:exported="false"/> |
| | | <activity |
| | | android:name=".ui.invitation.InvitationActivity" |
| | | android:exported="false"/> |
| | | |
| | | </application> |
| | | |
| | | </manifest> |
| | |
| | | import android.provider.Telephony |
| | | import android.widget.Toast |
| | | import androidx.activity.result.contract.ActivityResultContracts |
| | | import com.google.android.material.bottomnavigation.BottomNavigationView |
| | | import androidx.appcompat.app.AppCompatActivity |
| | | import androidx.core.content.ContextCompat |
| | | import androidx.navigation.findNavController |
| | | import androidx.navigation.ui.AppBarConfiguration |
| | | import androidx.navigation.ui.setupActionBarWithNavController |
| | | import androidx.navigation.ui.setupWithNavController |
| | | import com.example.firstapp.databinding.ActivityMainBinding |
| | | import com.example.firstapp.receiver.SmsReceiver |
| | | import android.Manifest |
| | | 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 android.os.Build |
| | | import androidx.annotation.RequiresApi |
| | | import androidx.work.ExistingPeriodicWorkPolicy |
| | | import androidx.work.PeriodicWorkRequestBuilder |
| | | import androidx.work.WorkManager |
| | |
| | | import com.example.firstapp.database.entity.Code |
| | | import com.example.firstapp.database.entity.Msg |
| | | import com.example.firstapp.database.service.RetrofitClient |
| | | import com.example.firstapp.entity.Rule |
| | | import com.example.firstapp.ui.home.HomeViewModel |
| | | import com.example.firstapp.utils.Log |
| | | import com.example.firstapp.workers.KeywordUpdateWorker |
| | |
| | | import kotlinx.coroutines.launch |
| | | import java.text.SimpleDateFormat |
| | | import java.time.LocalDateTime |
| | | import java.time.format.DateTimeFormatter |
| | | import java.util.Calendar |
| | | import java.util.Date |
| | | import java.util.Locale |
| | | import java.util.concurrent.TimeUnit |
| | | import java.time.ZoneId |
| | | |
| | | class MainActivity : AppCompatActivity() { |
| | | |
| | | // 安全防护关键词数组 |
| | | private var securityKeywordsList = emptyList<String>() |
| | | |
| | | private lateinit var binding: ActivityMainBinding |
| | | |
| | | private var smsReceiver:SmsReceiver? = null |
| | | private var smsReceiver: SmsReceiver? = null |
| | | |
| | | private lateinit var adapter: MyAdapter |
| | | private lateinit var homeViewModel: HomeViewModel |
| | | |
| | | private val multiplePermissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> |
| | | when { |
| | | permissions.getOrDefault(Manifest.permission.RECEIVE_SMS, false) && |
| | | permissions.getOrDefault(Manifest.permission.READ_SMS, false) -> { |
| | | // 两个权限都获得授权 |
| | | registerSmsReceiver() |
| | | private val multiplePermissionRequest = |
| | | registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> |
| | | when { |
| | | permissions.getOrDefault( |
| | | Manifest.permission.RECEIVE_SMS, |
| | | false |
| | | ) && permissions.getOrDefault(Manifest.permission.READ_SMS, false) -> { |
| | | // 两个权限都获得授权 |
| | | registerSmsReceiver() |
| | | // syncRecentSms() |
| | | initializeSecurityKeywords() |
| | | } |
| | | else -> { |
| | | // 有权限被拒绝 |
| | | Toast.makeText(this, "需要短信读取和接收权限才能正常使用功能", Toast.LENGTH_SHORT).show() |
| | | // initializeSecurityKeywords() |
| | | } |
| | | |
| | | else -> { |
| | | // 有权限被拒绝 |
| | | Toast.makeText( |
| | | this, "需要短信读取和接收权限才能正常使用功能", Toast.LENGTH_SHORT |
| | | ).show() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private val syncLock = Object() |
| | | |
| | | @RequiresApi(Build.VERSION_CODES.O) |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | |
| | |
| | | navView.setupWithNavController(navController) |
| | | |
| | | // 检查权限 |
| | | if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED || |
| | | ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { |
| | | if (ContextCompat.checkSelfPermission( |
| | | this, Manifest.permission.RECEIVE_SMS |
| | | ) != android.content.pm.PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission( |
| | | this, Manifest.permission.READ_SMS |
| | | ) != android.content.pm.PackageManager.PERMISSION_GRANTED |
| | | ) { |
| | | // 同时请求两个权限 |
| | | multiplePermissionRequest.launch(arrayOf( |
| | | Manifest.permission.RECEIVE_SMS, |
| | | Manifest.permission.READ_SMS |
| | | )) |
| | | multiplePermissionRequest.launch( |
| | | arrayOf( |
| | | Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS |
| | | ) |
| | | ) |
| | | } else { |
| | | // 权限已经授予,继续执行相关操作 |
| | | registerSmsReceiver() |
| | |
| | | |
| | | private fun logout() { |
| | | // 清除登录信息 |
| | | getSharedPreferences("user_info", Context.MODE_PRIVATE) |
| | | .edit() |
| | | .clear() |
| | | .apply() |
| | | getSharedPreferences("user_info", Context.MODE_PRIVATE).edit().clear().apply() |
| | | |
| | | // 跳转回登录页 |
| | | startActivity(Intent(this, LoginActivity::class.java)) |
| | |
| | | } |
| | | |
| | | // 初始化禁用词 |
| | | @RequiresApi(Build.VERSION_CODES.O) |
| | | private fun initializeSecurityKeywords() { |
| | | CoroutineScope(Dispatchers.IO).launch { |
| | | try { |
| | | val response = RetrofitClient.apiService.getSecurityList() |
| | | if (response.code == 200) { |
| | | securityKeywordsList = response.data.map { it.keyword } |
| | | android.util.Log.d("MainActivity", "securityKeywordsList: $securityKeywordsList") |
| | | android.util.Log.d( |
| | | "MainActivity", "securityKeywordsList: $securityKeywordsList" |
| | | ) |
| | | // 确保在主线程中调用 syncRecentSms |
| | | runOnUiThread { |
| | | syncRecentSms() |
| | | } |
| | | } else { |
| | | android.util.Log.e("MainActivity", "Failed to get security list: ${response.code}") |
| | | android.util.Log.e( |
| | | "MainActivity", "Failed to get security list: ${response.code}" |
| | | ) |
| | | } |
| | | } catch (e: Exception) { |
| | | android.util.Log.e("MainActivity", "Error fetching security list", e) |
| | |
| | | } |
| | | } |
| | | |
| | | @RequiresApi(Build.VERSION_CODES.O) |
| | | private fun syncRecentSms() { |
| | | |
| | | try { |
| | | val calendar = Calendar.getInstance() |
| | | calendar.add(Calendar.DAY_OF_YEAR, -3) // 获取3天前的时间 |
| | | calendar.add(Calendar.DAY_OF_YEAR, -3) |
| | | val threeDaysAgo = calendar.timeInMillis |
| | | |
| | | val cursor = contentResolver.query( |
| | | // Uri.parse("content://sms/sent"), //发送短信 |
| | | Uri.parse("content://sms/inbox"), |
| | | arrayOf("address", "body", "date"), |
| | | "date >= ?", |
| | |
| | | |
| | | cursor?.use { |
| | | while (cursor.moveToNext()) { |
| | | //手机号 |
| | | val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) |
| | | //短信内容 |
| | | val messageBody = cursor.getString(cursor.getColumnIndexOrThrow("body")) |
| | | //短信时间 |
| | | val datetime = cursor.getLong(cursor.getColumnIndexOrThrow("date")) |
| | | // 这里我要写个数组,并创建个对象存放一些内容,如这个对象的属性有匹配内容,正则表达式,并循环遍历 |
| | | val ruleList = mutableListOf( |
| | | Rule("快递","京东","\\d{6}"), |
| | | Rule("快递","菜鸟","\\d{1,2}-\\d{1,2}-\\d{4}") |
| | | ) |
| | | for (rule in ruleList) { |
| | | val code = rule.extractCodeFromMessage(messageBody.toString()) |
| | | if (code!==null) { |
| | | |
| | | // 转换为 Date 对象 |
| | | val date = Date(datetime) |
| | | val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) |
| | | val dateString = sdf.format(date) |
| | | |
| | | // 转换为 Date 对象 |
| | | val date = Date(datetime) |
| | | // 如果需要格式化显示 |
| | | val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) |
| | | val dateString = sdf.format(date) |
| | | val existingCode = Core.code.queryByTypeAndCodeAndDate(rule.content, code, dateString) |
| | | if (existingCode == null) { |
| | | android.util.Log.d("SmsReceiver", "Received SMS code: ${code}") |
| | | val msg = Msg(0, "1111", "111111", messageBody.toString(), 1, "111", 1, 1) |
| | | val msgId = Core.msg.insert(msg) |
| | | // 禁用关键词拦截,如果有禁用词则不保存在Code里面 |
| | | android.util.Log.d("首页SmsReceiver", "securityKeywordsList: $securityKeywordsList") |
| | | if (securityKeywordsList.any { it in messageBody.toString() }) { |
| | | android.util.Log.d("首页SmsReceiver", "Received SMS code: 禁用关键词拦截,不保存") |
| | | continue |
| | | // 保存原始短信 |
| | | val msg = Msg(0, "1111", "111111", messageBody, 1, "111", 1, 1) |
| | | val msgId = Core.msg.insert(msg) |
| | | |
| | | // 禁用关键词拦截 |
| | | if (securityKeywordsList.any { it in messageBody }) { |
| | | android.util.Log.d("MainActivity", "历史短信含有禁用关键词,跳过处理") |
| | | continue |
| | | } |
| | | |
| | | // 使用协程处理API调用和数据库操作 |
| | | CoroutineScope(Dispatchers.IO).launch { |
| | | try { |
| | | // API调用移到synchronized块外 |
| | | val response = RetrofitClient.apiService.processSms(mapOf("content" to messageBody)) |
| | | |
| | | // 数据库操作放在synchronized块内 |
| | | synchronized(syncLock) { |
| | | if (response.status == "success") { |
| | | when (response.data.category) { |
| | | "快递" -> { |
| | | val existingCode = Core.code.queryByTypeAndCodeAndDate( |
| | | response.data.category, |
| | | response.data.details.pickupCode ?: "", |
| | | dateString |
| | | ) |
| | | |
| | | if (existingCode == null) { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 1, |
| | | typeId = 1, |
| | | ruleId = 1, |
| | | msgId = msgId, |
| | | createTime = dateString, |
| | | oneLevel = response.data.details.post ?: "", |
| | | secondLevel = response.data.details.company ?: "", |
| | | code = response.data.details.pickupCode ?: "", |
| | | pickup = 0, |
| | | pickupTime = "", |
| | | overTime = "", |
| | | address = response.data.details.address ?: "", |
| | | remarks = response.data.details.time ?: "", |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | android.util.Log.d("MainActivity", "历史快递短信已保存: ${response.data.details.pickupCode}") |
| | | } else { |
| | | android.util.Log.d("MainActivity", "发现重复快递短信,跳过保存: ${response.data.details.pickupCode}") |
| | | } |
| | | } |
| | | "还款" -> { |
| | | val existingCode = Core.code.queryByTypeAndCodeAndDate( |
| | | response.data.category, |
| | | response.data.details.amount ?: "", |
| | | dateString |
| | | ) |
| | | |
| | | if (existingCode == null) { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 2, |
| | | typeId = 1, |
| | | ruleId = 2, |
| | | msgId = msgId, |
| | | createTime = dateString, |
| | | oneLevel = response.data.details.type ?: "", |
| | | secondLevel = response.data.details.bank ?: "", |
| | | code = response.data.details.amount ?: "", |
| | | pickup = 0, |
| | | pickupTime = "", |
| | | overTime = response.data.details.date ?: "", |
| | | address = response.data.details.address ?: "", |
| | | remarks = "最小还款金额${response.data.details.min_amount}还款卡号${response.data.details.number}" |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | android.util.Log.d("MainActivity", "历史还款短信已保存: ${response.data.details.amount}") |
| | | } else { |
| | | android.util.Log.d("MainActivity", "发现重复还款短信,跳过保存: ${response.data.details.amount}") |
| | | } |
| | | } |
| | | |
| | | "收入" -> { |
| | | val existingCode = Core.code.queryByTypeAndCodeAndDate( |
| | | response.data.category, |
| | | response.data.details.amount ?: "", |
| | | dateString |
| | | ) |
| | | |
| | | if (existingCode == null) { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 3, // 3-收入类型 |
| | | typeId = 1, //暂时没有根据type分类 |
| | | ruleId = 2, //1-还款类型 |
| | | msgId = msgId, |
| | | createTime = dateString, |
| | | oneLevel = response.data.details.bank ?: "", |
| | | secondLevel = response.data.details.bank ?: "", |
| | | code = response.data.details.amount ?: "", |
| | | pickup = 0, // 0-未取件,1-已取件 |
| | | pickupTime = "", // 取件时间为空 |
| | | overTime = response.data.details.datetime |
| | | ?: "", // 超时时间为空,暂时没有这块处理逻辑 |
| | | address = response.data.details.address ?: "", |
| | | remarks = "余额" + response.data.details.balance ?: "", |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | android.util.Log.d("MainActivity", "历史还款短信已保存: ${response.data.details.amount}") |
| | | } else { |
| | | android.util.Log.d("MainActivity", "发现重复还款短信,跳过保存: ${response.data.details.amount}") |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 发送广播通知数据已更新 |
| | | val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") |
| | | sendBroadcast(updateIntent) |
| | | } |
| | | val code = Code(0, rule.type, 1, rule.content, 1, 1, msgId, code, dateString, "中通",0,"","") |
| | | Core.code.insert(code) |
| | | android.util.Log.d("SMS_DEBUG", "历史短信已保存到数据库") |
| | | }else{ |
| | | android.util.Log.d("SmsReceiver", "Received SMS code: 已存在相同记录,不保存") |
| | | } |
| | | }else{ |
| | | android.util.Log.d("SmsReceiver", "Received SMS code: 没有匹配到历史短信内容") |
| | | } catch (e: Exception) { |
| | | android.util.Log.e("MainActivity", "处理历史短信出错: ${e.message}", e) |
| | | } |
| | | } |
| | | // 发送广播通知数据已更新 |
| | | val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") |
| | | sendBroadcast(updateIntent) |
| | | } |
| | | } |
| | | } catch (e: Exception) { |
| | |
| | | val packages = codes.map { code -> |
| | | ExpressPackage( |
| | | id = code.id, |
| | | company = code.name ?: company, |
| | | company = code.secondLevel ?: company, |
| | | trackingNumber = code.code, |
| | | createTime = code.createtime |
| | | createTime = code.createTime |
| | | ) |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.example.firstapp.adapter |
| | | |
| | | import android.view.LayoutInflater |
| | | import android.view.View |
| | | import android.view.ViewGroup |
| | | import android.widget.TextView |
| | | import androidx.recyclerview.widget.RecyclerView |
| | | import com.example.firstapp.R |
| | | import com.example.firstapp.entity.InvitationRecord |
| | | |
| | | class InvitationAdapter : RecyclerView.Adapter<InvitationAdapter.ViewHolder>() { |
| | | |
| | | private var records = emptyList<InvitationRecord>() |
| | | |
| | | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { |
| | | val message: TextView = itemView.findViewById(R.id.message) |
| | | } |
| | | |
| | | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
| | | val view = LayoutInflater.from(parent.context) |
| | | .inflate(R.layout.activity_invitation_success, parent, false) |
| | | return ViewHolder(view) |
| | | } |
| | | |
| | | override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
| | | val record = records[position] |
| | | holder.message.text = "${record.userName}邀请好友,${record.reward}" |
| | | } |
| | | |
| | | override fun getItemCount() = records.size |
| | | |
| | | fun submitList(newList: List<InvitationRecord>) { |
| | | records = newList |
| | | notifyDataSetChanged() |
| | | } |
| | | } |
| | |
| | | |
| | | override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
| | | val item = getItem(position) // 使用 getItem 来获取当前位置的 item |
| | | holder.tvTitle.text = item.type // 假设 Code 类有一个 `type` 属性 |
| | | holder.tvTitle.text = item.oneLevel // 假设 Code 类有一个 `type` 属性 |
| | | holder.tvDescription.text = item.code // 假设 Code 类有一个 `code` 属性 |
| | | var createtime = "请注意:当前取件免费截止时间是"+item.createtime+",超时会收取额外费用" |
| | | var createtime = "请注意:当前取件免费截止时间是"+item.createTime+",超时会收取额外费用" |
| | | holder.overTimeMsg.text = createtime |
| | | println("打印......") |
| | | } |
| | |
| | | |
| | | fun bind(code: Code) { |
| | | // imgCourier.setImageResource(code.category) |
| | | textCourierName.text = code.type |
| | | textCourierName.text = code.oneLevel |
| | | textTrackingNumber.text = code.code |
| | | // 步骤1:定义解析器,将字符串转为 Date |
| | | val parser = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) |
| | | val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) |
| | | try { |
| | | val date: Date? = parser.parse(code.createtime) // 解析字符串 |
| | | val date: Date? = parser.parse(code.createTime) // 解析字符串 |
| | | date?.let { |
| | | textTime.text = "到货:"+formatter.format(it) // 格式化并赋值 |
| | | } ?: run { |
| | |
| | | textTime.text = "Format Error" |
| | | } |
| | | try { |
| | | val date2: Date? = parser.parse(code.pickuptime) // 解析字符串 |
| | | val date2: Date? = parser.parse(code.pickupTime) // 解析字符串 |
| | | date2?.let { |
| | | textPickTime.text = "取件:"+formatter.format(it) // 格式化并赋值 |
| | | } ?: run { |
| | |
| | | @Query("SELECT * FROM Code") |
| | | fun getAllCodes(): List<Code> |
| | | |
| | | @Query("SELECT * FROM Code WHERE type = :type") |
| | | @Query("SELECT * FROM Code WHERE oneLevel = :type") |
| | | fun getCodesByType(type: String): List<Code> |
| | | |
| | | |
| | |
| | | |
| | | @Query(""" |
| | | SELECT * FROM Code |
| | | WHERE type LIKE '%' || :keyword || '%' |
| | | WHERE oneLevel LIKE '%' || :keyword || '%' |
| | | AND pickup = '0' |
| | | ORDER BY time DESC |
| | | """) |
| | | fun getByKeyword(keyword: String): List<Code> |
| | | |
| | | @Query("SELECT * FROM Code WHERE type = :content and code= :code and createtime = :dateString LIMIT 1") |
| | | @Query("SELECT * FROM Code WHERE oneLevel = :content and code= :code and createTime = :dateString LIMIT 1") |
| | | fun queryByTypeAndCodeAndDate(content: String, code: String, dateString: String): Code |
| | | |
| | | |
| | |
| | | fun pickup(id: Long) |
| | | |
| | | //查询当天包裹信息 |
| | | @Query("SELECT * FROM code WHERE date(createtime) = date(:date/1000, 'unixepoch', 'localtime') ORDER BY createtime DESC") |
| | | @Query("SELECT * FROM code WHERE date(createTime) = date(:date/1000, 'unixepoch', 'localtime') ORDER BY createTime DESC") |
| | | fun getNewPackagesByDay(date: Long): List<Code> |
| | | |
| | | @Query(""" |
| | | SELECT * FROM code |
| | | WHERE substr(createtime, 1, 10) = |
| | | WHERE substr(createTime, 1, 10) = |
| | | date(:date/1000, 'unixepoch', 'localtime') |
| | | ORDER BY createtime DESC |
| | | ORDER BY createTime DESC |
| | | """) |
| | | fun getPackagesByDay(date: Long): Flow<List<Code>> |
| | | |
| | | @Query(""" |
| | | SELECT type as courierName, COUNT(*) as count |
| | | SELECT oneLevel as courierName, COUNT(*) as count |
| | | FROM code |
| | | WHERE strftime('%Y-%W', substr(createtime, 1, 10)) = |
| | | WHERE strftime('%Y-%W', substr(createTime, 1, 10)) = |
| | | strftime('%Y-%W', datetime(:date/1000, 'unixepoch', 'localtime')) |
| | | GROUP BY type |
| | | GROUP BY oneLevel |
| | | ORDER BY count DESC |
| | | """) |
| | | fun getCourierStatsByWeek(date: Long): Flow<List<CourierStat>> |
| | |
| | | COUNT(c.id) as count, |
| | | date_value as weekStart |
| | | FROM dates d |
| | | LEFT JOIN code c ON strftime('%Y-%m-%d', c.createtime) BETWEEN |
| | | LEFT JOIN code c ON strftime('%Y-%m-%d', c.createTime) BETWEEN |
| | | strftime('%Y-%m-%d', d.date_value) |
| | | AND strftime('%Y-%m-%d', date(d.date_value, '+6 days')) |
| | | GROUP BY d.date_value |
| | |
| | | |
| | | @Query(""" |
| | | SELECT * FROM code |
| | | WHERE substr(createtime, 1, 10) = |
| | | WHERE substr(createTime, 1, 10) = |
| | | date(:date/1000, 'unixepoch', 'localtime') |
| | | ORDER BY createtime DESC |
| | | ORDER BY createTime DESC |
| | | """) |
| | | fun getPackagesByWeek(date: Long): Flow<List<Code>> |
| | | |
| | | @Query(""" |
| | | SELECT type as courierName, COUNT(*) as count |
| | | SELECT oneLevel as courierName, COUNT(*) as count |
| | | FROM code |
| | | WHERE strftime('%Y-%m', substr(createtime, 1, 10)) = |
| | | WHERE strftime('%Y-%m', substr(createTime, 1, 10)) = |
| | | strftime('%Y-%m', datetime(:date/1000, 'unixepoch', 'localtime')) |
| | | GROUP BY type |
| | | GROUP BY oneLevel |
| | | ORDER BY count DESC |
| | | """) |
| | | fun getCourierStatsByMonth(date: Long): Flow<List<CourierStat>> |
| | | |
| | | @Query(""" |
| | | SELECT type as courierName, COUNT(*) as count |
| | | SELECT oneLevel as courierName, COUNT(*) as count |
| | | FROM code |
| | | WHERE strftime('%Y', substr(createtime, 1, 10)) = |
| | | WHERE strftime('%Y', substr(createTime, 1, 10)) = |
| | | strftime('%Y', datetime(:date/1000, 'unixepoch', 'localtime')) |
| | | GROUP BY type |
| | | GROUP BY oneLevel |
| | | ORDER BY count DESC |
| | | """) |
| | | fun getCourierStatsByYear(date: Long): Flow<List<CourierStat>> |
| | |
| | | // COUNT(code.id) as count, |
| | | // month_start as week_start |
| | | // FROM months |
| | | // LEFT JOIN code ON strftime('%Y-%m', code.createtime) = strftime('%Y-%m', months.month_start) |
| | | // LEFT JOIN code ON strftime('%Y-%m', code.createTime) = strftime('%Y-%m', months.month_start) |
| | | // GROUP BY months.month_start |
| | | // ORDER BY months.month_start ASC |
| | | // """) |
| | |
| | | COUNT(code.id) as count, |
| | | year_start as week_start |
| | | FROM years |
| | | LEFT JOIN code ON strftime('%Y', code.createtime) = strftime('%Y', years.year_start) |
| | | LEFT JOIN code ON strftime('%Y', code.createTime) = strftime('%Y', years.year_start) |
| | | GROUP BY years.year_start |
| | | ORDER BY years.year_start ASC |
| | | """) |
| | |
| | | CAST(strftime('%W', dates.date) AS INTEGER) as weekOfYear, |
| | | COUNT(code.id) as count |
| | | FROM dates |
| | | LEFT JOIN code ON date(code.createtime) = dates.date |
| | | LEFT JOIN code ON date(code.createTime) = dates.date |
| | | GROUP BY dates.date |
| | | ORDER BY dates.date |
| | | """) |
| | |
| | | COUNT(code.id) as count, |
| | | days.date as week_start |
| | | FROM days |
| | | LEFT JOIN code ON date(code.createtime) = days.date |
| | | LEFT JOIN code ON date(code.createTime) = days.date |
| | | GROUP BY days.date |
| | | ORDER BY days.date ASC |
| | | """) |
| | |
| | | COUNT(code.id) as count, |
| | | month_start as weekStart |
| | | FROM months |
| | | LEFT JOIN code ON strftime('%Y-%m', code.createtime) = strftime('%Y-%m', months.month_start) |
| | | LEFT JOIN code ON strftime('%Y-%m', code.createTime) = strftime('%Y-%m', months.month_start) |
| | | GROUP BY months.month_start |
| | | ORDER BY months.month_start ASC |
| | | """) |
| | |
| | | COUNT(code.id) as count, |
| | | days.date as weekStart |
| | | FROM days |
| | | LEFT JOIN code ON date(code.createtime) = days.date |
| | | LEFT JOIN code ON date(code.createTime) = days.date |
| | | GROUP BY days.date |
| | | ORDER BY days.date ASC |
| | | """) |
| | |
| | | COUNT(id) as count, |
| | | datetime(:date/1000, 'unixepoch', 'localtime') as weekStart |
| | | FROM code |
| | | WHERE strftime('%Y', createtime) = strftime('%Y', datetime(:date/1000, 'unixepoch', 'localtime')) |
| | | WHERE strftime('%Y', createTime) = strftime('%Y', datetime(:date/1000, 'unixepoch', 'localtime')) |
| | | """) |
| | | fun getCurrentYearStats(date: Long): Flow<List<DailyStat>> |
| | | } |
| | |
| | | @PrimaryKey(autoGenerate = true) val id: Long = 0, // 自增长的 id |
| | | val category: String, |
| | | val categoryId: Long, |
| | | val type: String, |
| | | val typeId: Long, |
| | | val ruleId: Long, |
| | | val msgId: Long, |
| | | val code: String, |
| | | var createtime: String, |
| | | var name: String, |
| | | var pickup: Int, |
| | | var pickuptime: String, //取件时间 |
| | | var overtime: String, // 超期时间 |
| | | var createTime: String, |
| | | val oneLevel: String, //type改成oneLevel |
| | | var secondLevel: String, //name改成secondLevel |
| | | val code: String, //快递对应取件码、还款对应金额 |
| | | var pickup: Int, //快递取件状态 |
| | | var pickupTime: String, //快递取件时间 |
| | | var overTime: String, //快递超期时间、还款日期 |
| | | var address: String, //地址信息 |
| | | var remarks: String, //备注信息保存 |
| | | var time: Date = Date(), |
| | | ) |
对比新文件 |
| | |
| | | package com.example.firstapp.database.response |
| | | |
| | | import com.google.gson.annotations.SerializedName |
| | | |
| | | // 添加数据类来映射API响应 |
| | | data class SmsProcessResponse( |
| | | val status: String, |
| | | val data: ProcessedData |
| | | ) |
| | | |
| | | data class ProcessedData( |
| | | val category: String, |
| | | val details: Details |
| | | ) |
| | | |
| | | data class Details( |
| | | val post: String?, |
| | | val company: String?, |
| | | @SerializedName("pickup_code") |
| | | val pickupCode: String?, |
| | | val type: String?, |
| | | val bank: String?, |
| | | val amount: String?, |
| | | val date: String?, |
| | | val address: String?, |
| | | val time: String?, |
| | | val min_amount: String?, |
| | | val number: String?, |
| | | val datetime: String?, |
| | | val balance: String?, |
| | | ) |
| | |
| | | import com.example.firstapp.database.response.DictResponse |
| | | import com.example.firstapp.database.response.LoginResponse |
| | | import com.example.firstapp.database.response.SecurityResponse |
| | | import com.example.firstapp.database.response.SmsProcessResponse |
| | | import com.example.firstapp.database.response.UserInfo |
| | | import okhttp3.MultipartBody |
| | | import okhttp3.RequestBody |
| | | import retrofit2.Retrofit |
| | | import retrofit2.converter.gson.GsonConverterFactory |
| | | import retrofit2.http.Body |
| | | import retrofit2.http.GET |
| | | import retrofit2.http.Multipart |
| | | import retrofit2.http.POST |
| | |
| | | @Part("nickname") nickname: RequestBody, |
| | | @Part avatar: MultipartBody.Part? |
| | | ): ApiResponse<Unit> |
| | | |
| | | @POST("process-sms") |
| | | suspend fun processSms(@Body body: Map<String, String>): SmsProcessResponse |
| | | } |
| | | |
| | | // 创建Retrofit实例(单例) |
| | | object RetrofitClient{ |
| | | |
| | | private const val BASE_URL ="http://192.168.1.198:8888/jshERP-boot/" |
| | | // private const val BASE_URL ="http://192.168.1.213:8888/jshERP-boot/" |
| | | private const val BASE_URL ="http://192.168.1.213:5000/" |
| | | |
| | | //添加Gson解析器,用于自动将JSON响应转换为Kotlin/Java对象 |
| | | private val retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() |
对比新文件 |
| | |
| | | package com.example.firstapp.entity |
| | | |
| | | data class InvitationRecord( |
| | | val userName: String, |
| | | val reward: String |
| | | ) |
| | |
| | | |
| | | @DatabaseView( |
| | | """ |
| | | SELECT type as courierName, COUNT(*) as count |
| | | SELECT oneLevel as courierName, COUNT(*) as count |
| | | FROM Code |
| | | GROUP BY type |
| | | GROUP BY oneLevel |
| | | """ |
| | | ) |
| | | data class CourierStat( |
| | |
| | | |
| | | @DatabaseView( |
| | | """ |
| | | SELECT substr(createtime, 1, 10) as date, |
| | | SELECT substr(createTime, 1, 10) as date, |
| | | COUNT(*) as count |
| | | FROM code |
| | | GROUP BY substr(createtime, 1, 10) |
| | | GROUP BY substr(createTime, 1, 10) |
| | | """ |
| | | ) |
| | | data class DailyStat( |
| | |
| | | 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.database.service.RetrofitClient |
| | | import com.example.firstapp.entity.Rule |
| | | import kotlinx.coroutines.CoroutineScope |
| | | import kotlinx.coroutines.Dispatchers |
| | | import kotlinx.coroutines.launch |
| | | import java.text.SimpleDateFormat |
| | | import java.time.LocalDateTime |
| | | import java.time.ZoneId |
| | | import java.time.format.DateTimeFormatter |
| | | import java.util.Date |
| | | import java.util.Locale |
| | | |
| | |
| | | |
| | | @RequiresApi(Build.VERSION_CODES.O) |
| | | override fun onReceive(context: Context, intent: Intent) { |
| | | |
| | | // 检查广播的 Action 是否为短信接收 |
| | | if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION == intent.action) { |
| | | |
| | | CoroutineScope(Dispatchers.IO).launch { |
| | | try { |
| | | val response = RetrofitClient.apiService.getSecurityList(); |
| | | // 这里需要将response.data存放到变量中 |
| | | if(response.code===200){ |
| | | securityKeywordsList = response.data.map { it.keyword } |
| | | Log.d("SmsReceiver", "securityKeywordsList: $securityKeywordsList") |
| | | } |
| | | }catch (e: Exception){ |
| | | Log.d("SmsReceiver", "Error: ${e.message}") |
| | | } |
| | | } |
| | | |
| | | // 获取短信内容 |
| | | val bundle: Bundle? = intent.extras |
| | | bundle?.let { |
| | | val pdus = it.get("pdus") as Array<*> |
| | |
| | | messageBody.append(messages[i]?.messageBody) |
| | | } |
| | | |
| | | // 输出短信内容到控制台 |
| | | val msg = Msg(0, "1111", "111111", messageBody.toString(),1, "111", 1, 1) |
| | | // 保存原始短信 |
| | | val msg = Msg(0, "1111", "111111", messageBody.toString(), 1, "111", 1, 1) |
| | | val msgId = Core.msg.insert(msg) |
| | | |
| | | // 这里需要查看消息是否含有securityKeywordsList这个列表中的关键词,如果含有,则不进行下一步操作 |
| | |
| | | |
| | | Log.d("SmsReceiver", "运行到正则匹配") |
| | | |
| | | // 这里我要写个数组,并创建个对象存放一些内容,如这个对象的属性有匹配内容,正则表达式,并循环遍历 |
| | | val ruleList = mutableListOf( |
| | | Rule("快递","京东","\\d{6}"), |
| | | Rule("快递","菜鸟驿站","\\d{1,2}-\\d{1,2}-\\d{4}"), |
| | | // 银行规则使用默认正则,实际匹配时会使用 BANK_PATTERNS 中的模式 |
| | | Rule("财务", "中国银行", "账单金额[::](\\d+\\.?\\d*).*还款日[::](\\d{1,2})日"), |
| | | Rule("财务", "工商银行", "账单金额[::](\\d+\\.?\\d*).*还款日[::](\\d{1,2})日"), |
| | | Rule("财务", "建设银行", "账单金额[::](\\d+\\.?\\d*).*还款日[::](\\d{1,2})日"), |
| | | Rule("财务", "信用卡", "账单[¥¥](\\d+\\.?\\d*).*还款日(\\d{2})月(\\d{2})日"), |
| | | Rule("财务", "花呗", "本月花呗账单(\\d+\\.?\\d*)元.*还款日[期是::](\\d{1,2})[日号]") |
| | | ) |
| | | |
| | | // 调用API处理短信 |
| | | CoroutineScope(Dispatchers.IO).launch { |
| | | Log.d("SmsReceiver", "CoroutineScope started") |
| | | // 获取最新的关键词配置 |
| | | val keywords = Core.keyword.getKeywords() |
| | | Log.d("keywords", keywords.toString()) |
| | | keywords.forEach { keyword -> |
| | | ruleList.add( |
| | | Rule( |
| | | keyword.type, |
| | | keyword.keyword, |
| | | "\\d{1,2}-\\d{1,2}-\\d{4}" |
| | | ) |
| | | ) |
| | | } |
| | | Log.d("RuleList", ruleList.toString()) |
| | | try { |
| | | val response = |
| | | RetrofitClient.apiService.processSms(mapOf("content" to messageBody.toString())) |
| | | |
| | | for (rule in ruleList) { |
| | | val code = if (rule.type == "财务") { |
| | | // 对信用卡账单使用特殊的提取逻辑 |
| | | val regex = rule.pattern.toRegex() |
| | | val matchResult = regex.find(messageBody.toString()) |
| | | matchResult?.let { |
| | | val amount = it.groupValues[1] // 账单金额 |
| | | amount |
| | | } |
| | | } else { |
| | | rule.extractCodeFromMessage(messageBody.toString()) |
| | | } |
| | | |
| | | if (code !== null) { |
| | | if (response.status == "success") { |
| | | // 获取当前时间 |
| | | val currentTime = LocalDateTime.now() |
| | | val date = Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()) |
| | | val date = |
| | | Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()) |
| | | val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) |
| | | val createtime = sdf.format(date) |
| | | |
| | | // 对于信用卡账单,使用还款日作为time字段 |
| | | val time = if (rule.type == "财务") { |
| | | rule.extractDueDate(messageBody.toString()) ?: createtime |
| | | } else { |
| | | createtime |
| | | // 根据不同类型处理数据 |
| | | when (response.data.category) { |
| | | "快递" -> { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 1, // 1-快递类型 |
| | | typeId = 1, //暂时没有根据type分类 |
| | | ruleId = 1, //1-快递类型 |
| | | msgId = msgId, |
| | | createTime = createtime, |
| | | oneLevel = response.data.details.post ?: "", |
| | | secondLevel = response.data.details.company ?: "", |
| | | code = response.data.details.pickupCode ?: "", |
| | | pickup = 0, // 0-未取件,1-已取件 |
| | | pickupTime = "", // 取件时间为空 |
| | | overTime = "", // 超时时间为空,暂时没有这块处理逻辑 |
| | | address = response.data.details.address ?: "", |
| | | remarks = response.data.details.time ?: "", |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | } |
| | | |
| | | "还款" -> { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 2, // 2-还款类型 |
| | | typeId = 1, //暂时没有根据type分类 |
| | | ruleId = 2, //1-还款类型 |
| | | msgId = msgId, |
| | | createTime = createtime, |
| | | oneLevel = response.data.details.type ?: "", |
| | | secondLevel = response.data.details.bank ?: "", |
| | | code = response.data.details.amount ?: "", |
| | | pickup = 0, // 0-未取件,1-已取件 |
| | | pickupTime = "", // 取件时间为空 |
| | | overTime = response.data.details.date |
| | | ?: "", // 超时时间为空,暂时没有这块处理逻辑 |
| | | address = response.data.details.address ?: "", |
| | | remarks = "最小还款金额" + response.data.details.min_amount + "还款卡号" + response.data.details.number |
| | | ?: "", |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | } |
| | | |
| | | "收入" -> { |
| | | val code = Code( |
| | | id = 0, |
| | | category = response.data.category, |
| | | categoryId = 3, // 3-收入类型 |
| | | typeId = 1, //暂时没有根据type分类 |
| | | ruleId = 2, //1-还款类型 |
| | | msgId = msgId, |
| | | createTime = createtime, |
| | | oneLevel = response.data.details.bank ?: "", |
| | | secondLevel = response.data.details.bank ?: "", |
| | | code = response.data.details.amount ?: "", |
| | | pickup = 0, // 0-未取件,1-已取件 |
| | | pickupTime = "", // 取件时间为空 |
| | | overTime = response.data.details.datetime |
| | | ?: "", // 超时时间为空,暂时没有这块处理逻辑 |
| | | address = response.data.details.address ?: "", |
| | | remarks = "余额" + response.data.details.balance ?: "", |
| | | ) |
| | | if(code.oneLevel!="" && code.secondLevel!="" && code.code!="") { |
| | | Core.code.insert(code) |
| | | } |
| | | } |
| | | } |
| | | |
| | | val existingCode = Core.code.queryByTypeAndCodeAndDate(rule.content, code, createtime) |
| | | if (existingCode == null) { |
| | | val codeEntity = Code( |
| | | 0, |
| | | rule.type, |
| | | 1, |
| | | rule.content, |
| | | 1, |
| | | 1, |
| | | msgId, |
| | | code, |
| | | createtime, |
| | | rule.content, // 银行名称作为name |
| | | 0, |
| | | time, // 还款日期 |
| | | "" |
| | | ) |
| | | Core.code.insert(codeEntity) |
| | | 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: 已存在相同记录,不保存") |
| | | } |
| | | }else{ |
| | | Log.d("SmsReceiver", "Received SMS code: 没有匹配到内容") |
| | | // 发送广播通知数据已更新 |
| | | val updateIntent = Intent("com.example.firstapp.DATA_UPDATED") |
| | | context.sendBroadcast(updateIntent) |
| | | Log.d("SMS_DEBUG", "新短信已保存到数据库") |
| | | } |
| | | } catch (e: Exception) { |
| | | Log.e("SmsReceiver", "Error processing SMS", e) |
| | | } |
| | | } |
| | | |
| | | |
| | | // 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, 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: 没有匹配到内容") |
| | | // } |
| | | // } |
| | | |
| | | } |
| | | } |
| | | } |
| | |
| | | fun loadExpressData() { |
| | | viewModelScope.launch { |
| | | // 1. 获取所有驿站类型的提醒设置 |
| | | val stations = Core.reminder.getByType("驿站") |
| | | val stations = Core.reminder.getByType("快递") |
| | | |
| | | // 2. 按驿站分组获取包裹信息 |
| | | val groups = stations.map { station -> |
| | | val packages = Core.code.getByKeyword(station.nickname).map { code -> |
| | | ExpressPackage( |
| | | id = code.id, //ID |
| | | company = code.name, //快递公司 |
| | | company = code.secondLevel, //快递公司 |
| | | trackingNumber = code.code, // 取件码 |
| | | createTime = code.createtime //快递时间 |
| | | createTime = code.createTime //快递时间 |
| | | ) |
| | | } |
| | | ExpressGroup( |
| | |
| | | fun loadFinanceData() { |
| | | viewModelScope.launch { |
| | | // 1. 获取所有驿站类型的提醒设置 |
| | | val stations = Core.reminder.getByType("财务") |
| | | val stations = Core.reminder.getByType("还款") |
| | | |
| | | // 2. 按驿站分组获取包裹信息 |
| | | val groups = stations.map { station -> |
| | | val packages = Core.code.getByKeyword(station.nickname).map { code -> |
| | | FinancePackage( |
| | | id = code.id, //ID |
| | | company = code.name, //快递公司 |
| | | company = code.secondLevel, //快递公司 |
| | | trackingNumber = code.code, // 取件码 |
| | | createTime = code.createtime //快递时间 |
| | | createTime = code.createTime //快递时间 |
| | | ) |
| | | } |
| | | FinanceGroup( |
对比新文件 |
| | |
| | | package com.example.firstapp.ui.invitation |
| | | |
| | | import android.os.Bundle |
| | | import android.os.Handler |
| | | import android.os.Looper |
| | | import android.util.DisplayMetrics |
| | | import androidx.appcompat.app.AppCompatActivity |
| | | import androidx.recyclerview.widget.LinearLayoutManager |
| | | import androidx.recyclerview.widget.LinearSmoothScroller |
| | | import androidx.recyclerview.widget.RecyclerView |
| | | import com.example.firstapp.R |
| | | import com.example.firstapp.adapter.InvitationAdapter |
| | | import com.example.firstapp.entity.InvitationRecord |
| | | import java.text.SimpleDateFormat |
| | | import java.util.Locale |
| | | import java.util.concurrent.Executors |
| | | |
| | | class InvitationActivity : AppCompatActivity() { |
| | | |
| | | private lateinit var recyclerView: RecyclerView |
| | | private lateinit var adapter: InvitationAdapter |
| | | private var currentPosition = 0 |
| | | private val handler = Handler(Looper.getMainLooper()) |
| | | private lateinit var scrollRunnable: Runnable |
| | | |
| | | override fun onCreate(savedInstanceState: Bundle?) { |
| | | super.onCreate(savedInstanceState) |
| | | setContentView(R.layout.activity_invitation_main) |
| | | |
| | | setupRecyclerView() |
| | | loadData() |
| | | setupAutoScroll() |
| | | } |
| | | |
| | | private fun setupRecyclerView() { |
| | | recyclerView = findViewById(R.id.invitationsuccessRecyclerView) |
| | | recyclerView.layoutManager = LinearLayoutManager(this).apply { |
| | | stackFromEnd = false // 从顶部开始布局 |
| | | } |
| | | adapter = InvitationAdapter() |
| | | recyclerView.adapter = adapter |
| | | } |
| | | |
| | | private fun loadData() { |
| | | val mockData = listOf( |
| | | InvitationRecord("H****e", "获得了3天会员"), |
| | | InvitationRecord("U****r", "获得了7天会员"), |
| | | InvitationRecord("A****e", "获得了免广告特权"), |
| | | InvitationRecord("B****d", "获得了3天会员"), |
| | | InvitationRecord("C****o", "获得了7天会员") |
| | | ) |
| | | adapter.submitList(mockData) |
| | | } |
| | | |
| | | private fun setupAutoScroll() { |
| | | scrollRunnable = object : Runnable { |
| | | override fun run() { |
| | | if (currentPosition < adapter.itemCount - 1) { |
| | | currentPosition++ |
| | | smoothScrollToPosition(currentPosition) |
| | | } else { |
| | | // 滚动到底部后回到顶部 |
| | | currentPosition = 0 |
| | | recyclerView.scrollToPosition(0) |
| | | } |
| | | handler.postDelayed(this, 2000) |
| | | } |
| | | } |
| | | startAutoScroll() |
| | | } |
| | | |
| | | private fun smoothScrollToPosition(position: Int) { |
| | | val layoutManager = recyclerView.layoutManager as LinearLayoutManager |
| | | val smoothScroller = object : LinearSmoothScroller(this) { |
| | | override fun getVerticalSnapPreference(): Int = SNAP_TO_START |
| | | |
| | | override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float { |
| | | return 100f / displayMetrics.densityDpi // 控制滚动速度 |
| | | } |
| | | } |
| | | smoothScroller.targetPosition = position |
| | | layoutManager.startSmoothScroll(smoothScroller) |
| | | } |
| | | |
| | | private fun startAutoScroll() { |
| | | handler.postDelayed(scrollRunnable, 2000) |
| | | } |
| | | |
| | | private fun stopAutoScroll() { |
| | | handler.removeCallbacks(scrollRunnable) |
| | | } |
| | | |
| | | override fun onPause() { |
| | | super.onPause() |
| | | stopAutoScroll() |
| | | } |
| | | |
| | | override fun onResume() { |
| | | super.onResume() |
| | | startAutoScroll() |
| | | } |
| | | } |
对比新文件 |
| | |
| | | import android.content.Context |
| | | import android.util.DisplayMetrics |
| | | import androidx.recyclerview.widget.LinearSmoothScroller |
| | | |
| | | class LineByLineSmoothScroller(context: Context) : LinearSmoothScroller(context) { |
| | | override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float { |
| | | return 100f / displayMetrics.densityDpi // 控制滚动速度 |
| | | } |
| | | |
| | | override fun getVerticalSnapPreference(): Int { |
| | | return SNAP_TO_START // 从顶部开始对齐 |
| | | } |
| | | } |
| | |
| | | private fun setupClickListeners() { |
| | | |
| | | // 支付插件 |
| | | binding.payPlugin.setOnClickListener { |
| | | // 跳转到支付插件页面 |
| | | lifecycleScope.launch { |
| | | try { |
| | | val response = RetrofitClient.apiService.getPayOrderInfo() |
| | | var orderInfo=response.data |
| | | Log.d("AliPayHelper","获取订单信息时: ${response}") |
| | | // 这里调用支付宝 |
| | | PayAbility.aliPay(requireActivity(), orderInfo, Observer { |
| | | when (it.resultStatus) { |
| | | "9000" -> { |
| | | // Snackbar.make(binding.root, "支付成功", Snackbar.LENGTH_LONG).show() |
| | | requireActivity().runOnUiThread { |
| | | Toast.makeText(requireContext(), "支付成功", Toast.LENGTH_LONG).show() |
| | | } |
| | | } |
| | | else -> { |
| | | // Snackbar.make(binding.root, "支付失败", Snackbar.LENGTH_LONG).show() |
| | | requireActivity().runOnUiThread { |
| | | Toast.makeText(requireContext(), "支付失败", Toast.LENGTH_LONG).show() |
| | | } |
| | | |
| | | } |
| | | } |
| | | }) |
| | | |
| | | } catch (e: Exception) { |
| | | Log.d("AliPayHelper","获取订单信息时发生错误: ${e.message}") |
| | | } |
| | | } |
| | | } |
| | | // binding.payPlugin.setOnClickListener { |
| | | // // 跳转到支付插件页面 |
| | | // lifecycleScope.launch { |
| | | // try { |
| | | // val response = RetrofitClient.apiService.getPayOrderInfo() |
| | | // var orderInfo=response.data |
| | | // Log.d("AliPayHelper","获取订单信息时: ${response}") |
| | | // // 这里调用支付宝 |
| | | // PayAbility.aliPay(requireActivity(), orderInfo, Observer { |
| | | // when (it.resultStatus) { |
| | | // "9000" -> { |
| | | //// Snackbar.make(binding.root, "支付成功", Snackbar.LENGTH_LONG).show() |
| | | // requireActivity().runOnUiThread { |
| | | // Toast.makeText(requireContext(), "支付成功", Toast.LENGTH_LONG).show() |
| | | // } |
| | | // } |
| | | // else -> { |
| | | //// Snackbar.make(binding.root, "支付失败", Snackbar.LENGTH_LONG).show() |
| | | // requireActivity().runOnUiThread { |
| | | // Toast.makeText(requireContext(), "支付失败", Toast.LENGTH_LONG).show() |
| | | // } |
| | | // |
| | | // } |
| | | // } |
| | | // }) |
| | | // |
| | | // } catch (e: Exception) { |
| | | // Log.d("AliPayHelper","获取订单信息时发生错误: ${e.message}") |
| | | // } |
| | | // } |
| | | // } |
| | | |
| | | |
| | | // 设置提醒 |
| | |
| | | } |
| | | |
| | | private fun setupSpinner() { |
| | | val types = arrayOf("驿站", "财务", "其他") |
| | | val types = arrayOf("快递", "还款", "其他") |
| | | val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, types) |
| | | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) |
| | | binding.spinnerType.adapter = adapter |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <shape xmlns:android="http://schemas.android.com/apk/res/android"> |
| | | <solid android:color="@android:color/transparent" /> |
| | | <corners android:radius="10dp" /> |
| | | </shape> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="match_parent" |
| | | android:orientation="vertical" |
| | | android:background="#F0F0F0"> |
| | | |
| | | <!-- 标题栏 --> |
| | | <TextView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:text="邀请有礼" |
| | | android:textSize="20sp" |
| | | android:gravity="center" |
| | | android:padding="16dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <!-- 横幅广告区域 --> |
| | | <ImageView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="200dp" |
| | | android:src="@drawable/gift_one" |
| | | android:scaleType="fitXY" |
| | | android:layout_marginBottom="16dp" |
| | | android:background="@drawable/rounded_corner_image" |
| | | android:padding="1dp" /> |
| | | |
| | | <!-- 邀请成功提示区域 --> |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="vertical" |
| | | android:background="#FFFFFF" |
| | | android:layout_marginBottom="16dp"> |
| | | <androidx.recyclerview.widget.RecyclerView |
| | | android:id="@+id/invitationsuccessRecyclerView" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:maxHeight="100dp" |
| | | android:paddingStart="16dp" |
| | | android:paddingEnd="16dp" |
| | | android:paddingBottom="16dp" |
| | | android:clipToPadding="false" |
| | | android:overScrollMode="never"/> |
| | | </LinearLayout> |
| | | |
| | | <!-- 邀请按钮区域 --> |
| | | <Button |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:text="立即邀请好友" |
| | | android:backgroundTint="#FF0000" |
| | | android:textColor="#FFFFFF" |
| | | android:padding="16dp" |
| | | android:textSize="16sp" |
| | | android:layout_marginBottom="16dp"/> |
| | | |
| | | <!-- 邀请任务表格区域 --> |
| | | <TableLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:padding="16dp" |
| | | android:background="#FFFFFF" |
| | | android:layout_marginBottom="16dp"> |
| | | |
| | | <TableRow> |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="邀请人数" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="邀请奖励" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="被邀请人奖励" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | </TableRow> |
| | | |
| | | <TableRow> |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="1人" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="3天会员" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="1天会员" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | </TableRow> |
| | | |
| | | <TableRow> |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="3人" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="免广告特权 (7天)" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="1天会员" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | </TableRow> |
| | | |
| | | <TableRow> |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="5人" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="7天会员" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="1天会员" |
| | | android:textSize="16sp" |
| | | android:padding="8dp" |
| | | android:textColor="#333333"/> |
| | | </TableRow> |
| | | </TableLayout> |
| | | |
| | | <!-- 邀请记录区域 --> |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="vertical" |
| | | android:padding="16dp" |
| | | android:background="#FFFFFF"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="我的邀请记录" |
| | | android:textSize="16sp" |
| | | android:paddingBottom="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal" |
| | | android:paddingBottom="8dp"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="Hnnnnne" |
| | | android:textSize="16sp" |
| | | android:paddingEnd="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <Button |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="待注册" |
| | | android:background="#CCCCCC" |
| | | android:textColor="#000000" |
| | | android:padding="8dp" |
| | | android:textSize="14sp"/> |
| | | </LinearLayout> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal"> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="Hnnnnne" |
| | | android:textSize="16sp" |
| | | android:paddingEnd="8dp" |
| | | android:textColor="#333333"/> |
| | | |
| | | <Button |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:text="已注册" |
| | | android:background="#008800" |
| | | android:textColor="#FFFFFF" |
| | | android:padding="8dp" |
| | | android:textSize="14sp"/> |
| | | </LinearLayout> |
| | | </LinearLayout> |
| | | |
| | | </LinearLayout> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content" |
| | | android:orientation="horizontal" |
| | | android:paddingVertical="16dp"> |
| | | |
| | | <ImageView |
| | | android:id="@+id/avatar" |
| | | android:layout_width="40dp" |
| | | android:layout_height="40dp" |
| | | android:src="@drawable/avatar"/> |
| | | |
| | | <TextView |
| | | android:id="@+id/message" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:textSize="16sp" |
| | | android:paddingStart="8dp" |
| | | android:textColor="#333333"/> |
| | | </LinearLayout> |
| | |
| | | <ScrollView |
| | | android:layout_width="match_parent" |
| | | android:layout_height="match_parent" |
| | | android:layout_marginTop="80dp"> <!-- 留出顶部广告位的高度 --> |
| | | android:layout_marginTop="40dp"> <!-- 留出顶部广告位的高度 --> |
| | | > |
| | | |
| | | <!-- LinearLayout的作用是按照垂直或者水平方向排列其子视图--> |
| | |
| | | <ImageView |
| | | android:id="@+id/adBanner" |
| | | android:layout_width="match_parent" |
| | | android:layout_height="80dp" |
| | | android:layout_height="40dp" |
| | | android:scaleType="centerCrop" |
| | | android:src="@drawable/up"/> |
| | | </androidx.cardview.widget.CardView> |
| | |
| | | android:paddingHorizontal="8dp" |
| | | android:paddingVertical="4dp"> |
| | | |
| | | <!-- 会员 --> |
| | | <RelativeLayout |
| | | android:id="@+id/memberVip" |
| | | style="@style/PluginPay" |
| | | android:padding="16dp"> |
| | | <!-- <!– 会员 –>--> |
| | | <!-- <RelativeLayout--> |
| | | <!-- android:id="@+id/memberVip"--> |
| | | <!-- style="@style/PluginPay"--> |
| | | <!-- android:padding="16dp">--> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="会员" |
| | | android:textColor="@android:color/black" |
| | | android:textSize="16sp" /> |
| | | <!-- <TextView--> |
| | | <!-- android:layout_width="wrap_content"--> |
| | | <!-- android:layout_height="wrap_content"--> |
| | | <!-- android:layout_centerVertical="true"--> |
| | | <!-- android:text="会员"--> |
| | | <!-- android:textColor="@android:color/black"--> |
| | | <!-- android:textSize="16sp" />--> |
| | | |
| | | <ImageView |
| | | android:layout_width="24dp" |
| | | android:layout_height="24dp" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@drawable/right_forward" /> |
| | | </RelativeLayout> |
| | | <!-- <ImageView--> |
| | | <!-- android:layout_width="24dp"--> |
| | | <!-- android:layout_height="24dp"--> |
| | | <!-- android:layout_alignParentEnd="true"--> |
| | | <!-- android:layout_centerVertical="true"--> |
| | | <!-- android:src="@drawable/right_forward" />--> |
| | | <!-- </RelativeLayout>--> |
| | | |
| | | <!-- 支付 --> |
| | | <RelativeLayout |
| | | android:id="@+id/payPlugin" |
| | | style="@style/PluginPay" |
| | | android:padding="16dp"> |
| | | <!-- <!– 支付 –>--> |
| | | <!-- <RelativeLayout--> |
| | | <!-- android:id="@+id/payPlugin"--> |
| | | <!-- style="@style/PluginPay"--> |
| | | <!-- android:padding="16dp">--> |
| | | |
| | | <TextView |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:layout_centerVertical="true" |
| | | android:text="支付" |
| | | android:textColor="@android:color/black" |
| | | android:textSize="16sp" /> |
| | | <!-- <TextView--> |
| | | <!-- android:layout_width="wrap_content"--> |
| | | <!-- android:layout_height="wrap_content"--> |
| | | <!-- android:layout_centerVertical="true"--> |
| | | <!-- android:text="支付"--> |
| | | <!-- android:textColor="@android:color/black"--> |
| | | <!-- android:textSize="16sp" />--> |
| | | |
| | | <ImageView |
| | | android:layout_width="24dp" |
| | | android:layout_height="24dp" |
| | | android:layout_alignParentEnd="true" |
| | | android:layout_centerVertical="true" |
| | | android:src="@drawable/right_forward" /> |
| | | </RelativeLayout> |
| | | <!-- <ImageView--> |
| | | <!-- android:layout_width="24dp"--> |
| | | <!-- android:layout_height="24dp"--> |
| | | <!-- android:layout_alignParentEnd="true"--> |
| | | <!-- android:layout_centerVertical="true"--> |
| | | <!-- android:src="@drawable/right_forward" />--> |
| | | <!-- </RelativeLayout>--> |
| | | |
| | | <!-- 设置提醒 --> |
| | | <LinearLayout |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" |
| | | <com.google.android.material.card.MaterialCardView |
| | | 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="wrap_content" |
| | | android:layout_marginHorizontal="12dp" |
| | | android:layout_marginVertical="6dp" |
| | | app:cardCornerRadius="8dp" |
| | | app:cardElevation="2dp"> |
| | | app:cardElevation="2dp" |
| | | app:cardBackgroundColor="@android:color/white" |
| | | app:strokeColor="#FF000000" |
| | | app:strokeWidth="2dp"> |
| | | |
| | | <LinearLayout |
| | | android:layout_width="match_parent" |
| | |
| | | android:layout_width="match_parent" |
| | | android:layout_height="wrap_content"/> |
| | | </LinearLayout> |
| | | </androidx.cardview.widget.CardView> |
| | | </com.google.android.material.card.MaterialCardView> |
| | |
| | | android:id="@+id/tv_create_time" |
| | | android:layout_width="wrap_content" |
| | | android:layout_height="wrap_content" |
| | | android:textSize="12sp" |
| | | android:textSize="10sp" |
| | | android:textColor="#666666" |
| | | android:layout_marginTop="4dp"/> |
| | | </LinearLayout> |