Android 短信转发神器

2021-09-16  本文已影响0人  雁过留声_泪落无痕

如果你发现某平台有优惠活动,但是又愁手里的账号不够,这时你可能会想到请亲戚朋友帮忙,薅到羊毛后再请亲戚朋友搓一顿,那么你可能需要这个工具。

你用亲戚朋友的手机号注册了多个账号,但是平台限制每天都要登录一次(账号密码登录还不行,必须用验证码登录),那么你还得每天打电话找朋友要验证码,此时你就在想,要是他们收到短信后能自动发送给我该多好啊。

嗯,它来了~


如果你开了某酷会员,某女友也想用一下,又不想每次上班的时候被打扰要求你给验证码,那么你也可以使用此工具,验证码即可自动转发到她手机上去。

嗯,这次是真来了~

  1. 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))
    }
}
  1. 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)
            }
        }
    }

}
  1. 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)
        }
    }

}
  1. 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()
        }
    }

}
  1. 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.
    }
}
  1. 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)
    }
}
  1. 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)

}
  1. 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")
            }
        }
    }

}
  1. 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>
  1. 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'
}
  1. 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>
  1. 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" />
  1. 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>
  1. 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>
  1. 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>

某些系统上需要开启应用的自启动、关联启动和后台启动。

上一篇下一篇

猜你喜欢

热点阅读