2025年1月15日大约 5 分钟
build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.smsslide"
compileSdk = 34
defaultConfig {
applicationId = "com.example.smsslide"
minSdk = 34
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
viewBinding = true
}
}
dependencies {
implementation("androidx.cardview:cardview:1.0.0")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.appcompat)
implementation(libs.firebase.firestore.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
入口文件
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Request permission to read SMS messages -->
<uses-permission android:name="android.permission.READ_SMS"
tools:ignore="PermissionImpliesUnsupportedChromeOsHardware" />
<!-- Request permission to send SMS messages -->
<uses-permission android:name="android.permission.RECEIVE_SMS"
tools:ignore="PermissionImpliesUnsupportedChromeOsHardware" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SMSSlide"
>
<activity android:name=".DeletedSmsActivity"
android:theme="@style/AppTheme">
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.SMSSlide">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Java 文件夹
MainActivity.kt
package com.example.smsslide
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import android.Manifest
import android.util.Log
import android.content.pm.PackageManager
import android.database.Cursor
import android.provider.Telephony
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.PagerSnapHelper
import java.text.SimpleDateFormat
import java.util.*
class MainActivity : ComponentActivity() {
private lateinit var smsList: MutableList<SmsItem>
private var deletedSmsList = mutableListOf<SmsItem>() // List to hold recently deleted SMS
private lateinit var adapter: SmsAdapter
private lateinit var currentTimeTextView: TextView
private lateinit var fromAddressTextView: TextView
private var deletedSmsCount = 0 // Track the count of deleted SMS
// Register the permission request callback
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
loadSms()
} else {
Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI components
currentTimeTextView = findViewById(R.id.currentTimeTextView)
fromAddressTextView = findViewById(R.id.fromAddressTextView)
// val viewDeletedSmsButton = findViewById<ImageButton>(R.id.viewDeletedSmsButton)
// 获取自定义布局中的 TextView 并设置角标数量
val viewDeletedSmsButtonWithBadge = findViewById<View>(R.id.viewDeletedSmsButtonWithBadge)
val imageButton = viewDeletedSmsButtonWithBadge.findViewById<ImageButton>(R.id.imageButton)
// Initialize deletedSmsCount based on the current state of deletedSmsList
deletedSmsCount = deletedSmsList.size
updateDeletedSmsBadge()
// 设置 RecyclerView 适配器等其他逻辑
// ...
// 示例:设置点击事件
// backButton.setOnClickListener {
// // 返回上一个活动或处理返回逻辑
// }
// applyButton.setOnClickListener {
// // 处理应用按钮点击事件
// }
imageButton.setOnClickListener {
// 处理查看已删除短信按钮点击事件
startDeletedSmsActivity()
}
// viewDeletedSmsButton.setOnClickListener {
// startDeletedSmsActivity()
// }
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.READ_SMS)
} else {
loadSms()
}
}
private fun updateDeletedSmsBadge() {
val viewDeletedSmsButtonWithBadge = findViewById<View>(R.id.viewDeletedSmsButtonWithBadge)
val badgeTextView = viewDeletedSmsButtonWithBadge.findViewById<TextView>(R.id.badgeTextView)
if (deletedSmsCount > 0) {
badgeTextView.visibility = View.VISIBLE
badgeTextView.text = String.format(Locale.getDefault(), "%d", deletedSmsCount)
} else {
badgeTextView.visibility = View.GONE
}
}
private fun loadSms() {
smsList = mutableListOf()
val cursor: Cursor? = contentResolver.query(
Telephony.Sms.CONTENT_URI,
null,
null,
null,
Telephony.Sms.DEFAULT_SORT_ORDER
)
try {
cursor?.let {
while (it.moveToNext()) {
val address = it.getString(it.getColumnIndexOrThrow(Telephony.Sms.ADDRESS))
val body = it.getString(it.getColumnIndexOrThrow(Telephony.Sms.BODY))
val receivedTimeMillis = it.getLong(it.getColumnIndexOrThrow(Telephony.Sms.DATE))
val receivedTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss ", Locale.getDefault()).format(receivedTimeMillis)
smsList.add(SmsItem(address, body, receivedTime))
}
}
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, "Failed to read SMS", Toast.LENGTH_SHORT).show()
} finally {
cursor?.close()
}
setupRecyclerView()
}
private fun setupRecyclerView() {
val recyclerView = findViewById <RecyclerView>(R.id.recyclerView)
adapter = SmsAdapter(smsList) { selectedSms ->
updateCurrentTime(selectedSms.receivedTime, selectedSms.address)
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
// Attach PagerSnapHelper to ensure snapping to items
val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisiblePosition = findCenterViewPosition(layoutManager, snapHelper)
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
val selectedSms = smsList [firstVisiblePosition]
updateCurrentTime(selectedSms.receivedTime, selectedSms.address)
}
}
})
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP
) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
Log.d("MainActivity", " Swiped position: $position ")
if (position == RecyclerView.NO_POSITION) {
Toast.makeText(this@MainActivity, "Invalid position", Toast.LENGTH_SHORT).show()
Log.e("MainActivity", "Invalid position")
return
}
when (direction) {
ItemTouchHelper.UP -> {
// Move to trash
val removedSms = smsList [position]
deletedSmsList.add(removedSms) // Add to deleted list
deletedSmsCount++
smsList.removeAt(position)
adapter.notifyItemRemoved(position)
adapter.notifyItemRangeChanged(position, smsList.size - position)
Toast.makeText(this@MainActivity, "SMS Moved to Trash", Toast.LENGTH_SHORT).show()
Log.d("MainActivity", "SMS Moved to Trash: ${removedSms.address} at position $ position")
updateDeletedSmsBadge() // Update the UI
}
// override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// val position = viewHolder.bindingAdapterPosition
// if (position == RecyclerView.NO_POSITION) return
//
// when (direction) {
// ItemTouchHelper.UP -> {
// // Move to trash
// val removedSms = smsList [position]
// deletedSmsList.add(removedSms) // Add to deleted list
// smsList.removeAt(position)
// adapter.notifyItemRemoved(position)
// Toast.makeText(this@MainActivity, "SMS Moved to Trash", Toast.LENGTH_SHORT).show()
// }
ItemTouchHelper.LEFT, ItemTouchHelper.RIGHT -> {
// Left/Right swipe to change message
adapter.notifyItemChanged(position)
}
}
}
}
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun findCenterViewPosition(layoutManager: LinearLayoutManager, snapHelper: PagerSnapHelper): Int {
val centerView = snapHelper.findSnapView(layoutManager)
return centerView?.let { layoutManager.getPosition(it) } ?: RecyclerView.NO_POSITION
}
private fun updateCurrentTime(receivedTime: String, address: String) {
currentTimeTextView.text = getString(R.string.sms_received_time_label, receivedTime)
fromAddressTextView.text = getString(R.string.sms_from_address_label, address)
}
private fun startDeletedSmsActivity() {
val intent = Intent(this, DeletedSmsActivity:: class.java)
intent.putExtra("DELETED_SMS_LIST", ArrayList(deletedSmsList))
startActivity(intent)
}
}
SmsAdapter
package com.example.smsslide
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class SmsAdapter(private val smsList: List <SmsItem>, private val onSelect: (SmsItem) -> Unit) : RecyclerView.Adapter <SmsAdapter.SmsViewHolder>() {
class SmsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// val textView: TextView = itemView.findViewById(R.id.textView)
val addressTextView: TextView = itemView.findViewById(R.id.addressTextView)
val bodyTextView: TextView = itemView.findViewById(R.id.bodyTextView)
val receivedTimeTextView: TextView = itemView.findViewById(R.id.receivedTimeTextView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SmsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_sms, parent, false)
return SmsViewHolder(view)
}
override fun onBindViewHolder(holder: SmsViewHolder, position: Int) {
val smsItem = smsList [position]
holder.addressTextView.text = smsItem.address
holder.bodyTextView.text = smsItem.body
holder.receivedTimeTextView.text = smsItem.receivedTime
// val details = holder.itemView.context.getString(
// R.string.sms_details_format,
// smsItem.body
// )
// holder.textView.text = details
// Set click listener to update current time and from address in MainActivity
holder.itemView.setOnClickListener {
onSelect(smsItem)
}
}
override fun getItemCount(): Int {
return smsList.size
}
}
SmsItem
package com.example.smsslide
import java.io.Serializable
data class SmsItem(val address: String, val body: String, val receivedTime: String) : Serializable
DeletedSmsActivity
package com.example.smsslide
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class DeletedSmsActivity : AppCompatActivity() {
private lateinit var deletedSmsList: MutableList <SmsItem>
private lateinit var adapter: SmsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_deleted_sms)
// Retrieve the deleted SMS list from MainActivity
@Suppress("UNCHECKED_CAST")
deletedSmsList = intent.getSerializableExtra("DELETED_SMS_LIST") as MutableList <SmsItem>
setupRecyclerView()
}
private fun setupRecyclerView() {
val recyclerView = findViewById <RecyclerView>(R.id.deletedSmsRecyclerView)
adapter = SmsAdapter(deletedSmsList) { selectedSms ->
// Handle click event if needed
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
}
res文件夹
layout文件
activity_deleted_sms.xml
<?xml version="1.0" encoding="utf-8"?> < RelativeLayout xmlns: android = "http://schemas.android.com/apk/res/android" android: layout_width = "match_parent" android: layout_height = "match_parent" > < TextView android: id = "@+id/deletedSmsTitle" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "@string/recently_deleted_sms" android: textSize = "24sp" android: layout_marginTop = "16dp" android: layout_centerHorizontal = "true"/> < androidx.recyclerview.widget.RecyclerView android: id = "@+id/deletedSmsRecyclerView" android: layout_width = "match_parent" android: layout_height = "match_parent" android: layout_below = "@id/deletedSmsTitle" android: layout_weight = "1" android: layout_marginTop = "16dp" android: layout_marginBottom = "16dp"/> </RelativeLayout>
activity_main.xml
<?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: padding = "16dp" > <!-- First row: Back button, Current Time, Trash button --> < LinearLayout android: layout_width = "match_parent" android: layout_height = "wrap_content" android: orientation = "horizontal" android: gravity = "center_vertical" > < ImageButton android: id = "@+id/backButton" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: src = "@drawable/baseline_arrow_back_24" android: contentDescription = "@string/back"/> < LinearLayout android: layout_width = "0dp" android: layout_height = "wrap_content" android: orientation = "vertical" android: gravity = "center_horizontal" android: layout_weight = "1" > < TextView android: id = "@+id/currentTimeTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "" android: textSize = "18sp" android: gravity = "center_vertical"/> < TextView android: id = "@+id/fromAddressTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "" android: textSize = "18sp" android: gravity = "center_vertical"/> </LinearLayout> < include layout = "@layout/trash_image_button_with_badge" android: id = "@+id/viewDeletedSmsButtonWithBadge"/> </LinearLayout> <!-- Second row: Large block for SMS content --> < androidx.recyclerview.widget.RecyclerView android: id = "@+id/recyclerView" android: layout_width = "match_parent" android: layout_height = "0dp" android: layout_weight = "1" android: layout_marginTop = "16dp" android: layout_marginBottom = "16dp"/> <!-- Third row: Apply button --> < Button android: id = "@+id/applyButton" android: layout_width = "match_parent" android: layout_height = "wrap_content" android: text = "@string/apply" android: textSize = "18sp"/> </LinearLayout>
item_sms.xml
<?xml version="1.0" encoding="utf-8"?> < androidx.cardview.widget.CardView 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_marginBottom = "16dp" app: cardCornerRadius = "8dp" app: cardElevation = "4dp" > < LinearLayout android: layout_width = "match_parent" android: layout_height = "wrap_content" android: orientation = "vertical" android: padding = "16dp" > < TextView android: id = "@+id/addressTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "@string/address" android: textSize = "18sp" android: textStyle = "bold"/> < TextView android: id = "@+id/bodyTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "@string/body" android: textSize = "16sp" android: layout_marginTop = "4dp"/> < TextView android: id = "@+id/receivedTimeTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "@string/received_time" android: textSize = "14sp" android: layout_marginTop = "4dp" android: textColor = "#757575"/> </LinearLayout> </androidx.cardview.widget.CardView>
trash_image_button_with_badge.xml
<?xml version="1.0" encoding="utf-8"?> < FrameLayout xmlns: android = "http://schemas.android.com/apk/res/android" android: layout_width = "wrap_content" android: layout_height = "wrap_content" > < ImageButton android: id = "@+id/imageButton" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: src = "@drawable/baseline_delete_24" android: contentDescription = "@string/trash"/> < TextView android: id = "@+id/badgeTextView" android: layout_width = "wrap_content" android: layout_height = "wrap_content" android: text = "0" android: textSize = "12sp" android: textColor = "#FFFFFF" android: background = "@drawable/red_circle_background" android: paddingStart = "6dp" android: paddingEnd = "6dp" android: gravity = "center" android: layout_gravity = "top|end" android: visibility = "gone"/> </FrameLayout>