Android 短信转发神器
2021-09-16 本文已影响0人
雁过留声_泪落无痕
如果你发现某平台有优惠活动,但是又愁手里的账号不够,这时你可能会想到请亲戚朋友帮忙,薅到羊毛后再请亲戚朋友搓一顿,那么你可能需要这个工具。
你用亲戚朋友的手机号注册了多个账号,但是平台限制每天都要登录一次(账号密码登录还不行,必须用验证码登录),那么你还得每天打电话找朋友要验证码,此时你就在想,要是他们收到短信后能自动发送给我该多好啊。
嗯,它来了~
如果你开了某酷会员,某女友也想用一下,又不想每次上班的时候被打扰要求你给验证码,那么你也可以使用此工具,验证码即可自动转发到她手机上去。
嗯,这次是真来了~
- MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtils.d("onCreate()")
setContentView(R.layout.main_activity)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment.newInstance())
.commitNow()
}
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
it.values.forEach { granted ->
if (!granted) {
finish()
}
}
}.launch(arrayOf(Manifest.permission.RECEIVE_SMS, Manifest.permission.SEND_SMS))
}
}
- MainFragment
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var mBinding: MainFragmentBinding
private lateinit var mViewModel: MainViewModel
private lateinit var mAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = MainFragmentBinding.inflate(inflater)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mViewModel.getAllStrategies(requireContext()).observe(viewLifecycleOwner) {
mBinding.emptyView.isVisible = it.isEmpty()
mAdapter.setNewInstance(ArrayList(it))
}
mViewModel.getAllStrategies(requireContext())
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_main_fragment, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.add_strategy) {
val dialog = AddStrategyDialog.newInstance()
dialog.isCancelable = false
dialog.show(childFragmentManager, "AddStrategyDialog")
}
return super.onOptionsItemSelected(item)
}
private fun initViews() {
mAdapter = MyAdapter()
mBinding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
mBinding.recyclerView.adapter = mAdapter
}
private inner class MyAdapter :
BaseQuickAdapter<Strategy, BaseViewHolder>(R.layout.item_strategy) {
override fun convert(holder: BaseViewHolder, item: Strategy) {
holder.setText(R.id.send_target, item.senTarget)
holder.setText(R.id.match_words, item.matchWords.words)
holder.setText(R.id.with_suffix, item.withSuffix)
holder.getView<View>(R.id.close_button).setOnClickListener {
mViewModel.deleteStrategy(requireContext(), item)
}
}
}
}
- MainViewModel
class MainViewModel : ViewModel() {
private val mExecutor = Executors.newSingleThreadExecutor()
private lateinit var mStrategyLiveData: LiveData<List<Strategy>>
fun getAllStrategies(context: Context): LiveData<List<Strategy>> {
if (!this::mStrategyLiveData.isInitialized) {
mStrategyLiveData = AppDatabase.getAppDataBase(context).StrategyDao().getAll()
}
return mStrategyLiveData
}
fun addStrategy(context: Context, strategy: Strategy) {
mExecutor.execute {
AppDatabase.getAppDataBase(context).StrategyDao().insertAll(strategy)
}
}
fun deleteStrategy(context: Context, strategy: Strategy) {
mExecutor.execute {
AppDatabase.getAppDataBase(context).StrategyDao().delete(strategy)
}
}
}
- AddStrategyDialog
class AddStrategyDialog : DialogFragment() {
companion object {
fun newInstance() = AddStrategyDialog()
}
private var mOnDismissListener: DialogInterface.OnDismissListener? = null
private lateinit var mBinding: AddStrategyDialogBinding
private lateinit var mViewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = AddStrategyDialogBinding.inflate(layoutInflater)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
init()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
mOnDismissListener?.onDismiss(dialog)
}
fun setOnDismissListener(onDismissListener: DialogInterface.OnDismissListener) {
mOnDismissListener = onDismissListener
}
private fun init() {
mViewModel =
ViewModelProvider(parentFragment as ViewModelStoreOwner).get(MainViewModel::class.java)
mBinding.cancelButton.setOnClickListener {
dismiss()
}
mBinding.confirmButton.setOnClickListener {
val sendTarget = mBinding.sendTarget.text.trim().toString()
if (TextUtils.isEmpty(sendTarget)) {
ToastUtils.showShort("发送目标不能为空")
return@setOnClickListener
}
if (!RegexUtils.isMobileExact(sendTarget)) {
ToastUtils.showShort("发送目标不是有效的电话号码")
return@setOnClickListener
}
val matchWords = mBinding.matchWords.text.trim().toString()
if (TextUtils.isEmpty(matchWords)) {
ToastUtils.showShort("匹配词语不能为空")
return@setOnClickListener
}
val strategy = Strategy(
0,
sendTarget,
MatchWords(matchWords),
mBinding.withSuffix.text.toString()
)
mViewModel.addStrategy(requireContext(), strategy)
dismiss()
}
}
}
- AppDatabase
@Database(entities = [Strategy::class], version = 2 /*, exportSchema = false*/)
abstract class AppDatabase : RoomDatabase() {
companion object {
private var mAppDatabase: AppDatabase? = null
@JvmStatic
fun getAppDataBase(context: Context): AppDatabase {
if (null == mAppDatabase) {
mAppDatabase = Room.databaseBuilder(
context.applicationContext, AppDatabase::class.java, "sms_forward.db"
).addMigrations(MIGRATION_1_2).build()
}
return mAppDatabase!!
}
}
abstract fun StrategyDao(): StrategyDao
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// do nothing.
}
}
- Strategy
@Entity
@TypeConverters(StrategyConverter::class)
data class Strategy(
@PrimaryKey(autoGenerate = true) val uid: Int,
@ColumnInfo(name = "send_target") val senTarget: String,
@ColumnInfo(name = "match_words") val matchWords: MatchWords,
@ColumnInfo(name = "with_suffix") val withSuffix: String?,
)
class MatchWords(val words: String) {
fun getWords(): List<String> {
return words.split(" ")
}
}
class StrategyConverter {
@TypeConverter
fun objectToString(matchWords: MatchWords): String {
return matchWords.words
}
@TypeConverter
fun stringToObject(str: String): MatchWords {
return MatchWords(str)
}
}
- StrategyDao
@Dao
interface StrategyDao {
@Query("SELECT * FROM Strategy")
fun getAll(): LiveData<List<Strategy>>
@Query("SELECT * FROM Strategy WHERE uid IN (:ids)")
fun loadAllByIds(ids: IntArray): LiveData<List<Strategy>>
/*@Query("SELECT * FROM Strategy WHERE xxx LIKE :first AND xxx LIKE :last LIMIT 1")
fun findByName(first: String, last: String): Strategy*/
@Insert
fun insertAll(vararg strategies: Strategy)
@Delete
fun delete(strategy: Strategy)
}
- SMSReceiver
class SMSReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
LogUtils.d("SMSReceiver#onReceive()")
val strategyDao = AppDatabase.getAppDataBase(context).StrategyDao().getAll()
if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION == intent.action) {
val messages: Array<SmsMessage> = getMessagesFromIntent(intent)
strategyDao.observeForever {
it.forEach { strategy ->
for (message in messages) {
strategy.matchWords.getWords().forEach { word ->
if (message.displayMessageBody.contains(word)) {
forwardSMS(message.displayMessageBody, strategy)
}
}
}
}
}
}
}
private fun forwardSMS(body: String, strategy: Strategy) {
val content: String = body + strategy.withSuffix
val phone: String = strategy.senTarget
if (!TextUtils.isEmpty(content) && !TextUtils.isEmpty(phone)) {
val manager = SmsManager.getDefault()
manager.divideMessage(content).forEach {
manager.sendTextMessage(phone, null, it, null, null)
LogUtils.d("send sms, to: ${phone}, content: $it")
}
}
}
}
- AndroidManifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xxx">
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SMSForward">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".receiver.SMSReceiver">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
</manifest>
- build.gradle
android {
compileSdk 30
defaultConfig {
applicationId "xxx"
minSdk 21
targetSdk 28
versionCode 1
versionName "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation" : "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
viewBinding {
enabled = true
}
signingConfigs {
release {
xxx
}
}
buildTypes {
debug {
signingConfig signingConfigs.release
}
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.activity:activity-ktx:1.3.1"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
def room_version = "2.3.0"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'com.blankj:utilcodex:1.30.5'
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.2'
}
- menu_main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/add_strategy"
android:icon="@drawable/ic_baseline_add_24"
android:title="@string/add_strategy"
app:showAsAction="always" />
</menu>
- main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
- main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="未添加策略"
android:textSize="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- add_strategy_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="发送目标" />
<EditText
android:id="@+id/send_target"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:minWidth="200dp"
android:textSize="14dp"
tools:text="18888888888" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="包含文字" />
<EditText
android:id="@+id/match_words"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:hint="多个请用空格分开"
android:textSize="14dp"
tools:text="This That" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="携带后缀" />
<EditText
android:id="@+id/with_suffix"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:textSize="14dp"
tools:text="[by hehe]" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:gravity="end">
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="取消" />
<Button
android:id="@+id/confirm_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:text="确定" />
</LinearLayout>
</LinearLayout>
- item_strategy.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="发送目标" />
<TextView
android:id="@+id/send_target"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
tools:text="18828078637" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="包含文字" />
<TextView
android:id="@+id/match_words"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
tools:text="This, That, These, Those" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="携带后缀" />
<TextView
android:id="@+id/with_suffix"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
tools:text="by hehe" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e2e2e2" />
</LinearLayout>
<ImageButton
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|top"
android:src="@drawable/ic_baseline_close_24" />
</FrameLayout>
某些系统上需要开启应用的自启动、关联启动和后台启动。