新增:自动任务·快捷指令 —— 通道推送动作(开发中)

This commit is contained in:
pppscn 2023-12-12 13:42:04 +08:00
parent 51129509f9
commit e03a9b8198
6 changed files with 475 additions and 443 deletions

View File

@ -1,46 +1,34 @@
package com.idormy.sms.forwarder.fragment.action package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Looper import android.content.Intent
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.fragment.app.viewModels import com.google.gson.Gson
import androidx.lifecycle.Observer
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.spinner.AppListAdapterItem
import com.idormy.sms.forwarder.adapter.spinner.AppListSpinnerAdapter
import com.idormy.sms.forwarder.adapter.spinner.SenderAdapterItem import com.idormy.sms.forwarder.adapter.spinner.SenderAdapterItem
import com.idormy.sms.forwarder.adapter.spinner.SenderSpinnerAdapter import com.idormy.sms.forwarder.adapter.spinner.SenderSpinnerAdapter
import com.idormy.sms.forwarder.core.BaseFragment import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.AppDatabase import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory import com.idormy.sms.forwarder.databinding.FragmentTasksActionNotificationBinding
import com.idormy.sms.forwarder.database.viewmodel.RuleViewModel
import com.idormy.sms.forwarder.databinding.FragmentRulesEditBinding
import com.idormy.sms.forwarder.entity.MsgInfo import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.* import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xrouter.utils.TextUtils import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.utils.ResUtils import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.actionbar.TitleBar import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.picker.widget.builder.OptionsPickerBuilder import com.xuexiang.xui.widget.picker.widget.builder.OptionsPickerBuilder
import com.xuexiang.xui.widget.picker.widget.listener.OnOptionsSelectListener import com.xuexiang.xui.widget.picker.widget.listener.OnOptionsSelectListener
import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner
import com.xuexiang.xutil.XUtil
import io.reactivex.SingleObserver import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -50,14 +38,15 @@ import java.util.*
@Page(name = "Notification") @Page(name = "Notification")
@Suppress("PrivatePropertyName", "DEPRECATION") @Suppress("PrivatePropertyName", "DEPRECATION")
class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClickListener, CompoundButton.OnCheckedChangeListener { class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding?>(), View.OnClickListener, CompoundButton.OnCheckedChangeListener {
private val TAG: String = NotificationFragment::class.java.simpleName private val TAG: String = NotificationFragment::class.java.simpleName
var titleBar: TitleBar? = null var titleBar: TitleBar? = null
private val viewModel by viewModels<RuleViewModel> { BaseViewModelFactory(context) } private var mCountDownHelper: CountDownButtonHelper? = null
var callType = 1 @JvmField
var callTypeIndex = 0 @AutoWired(name = KEY_EVENT_DATA_ACTION)
var eventData: String? = null
//免打扰(禁用转发)时间段 //免打扰(禁用转发)时间段
private val mTimeOption = DataProvider.timePeriodOption private val mTimeOption = DataProvider.timePeriodOption
@ -74,25 +63,8 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
private val senderSpinnerList = ArrayList<SenderAdapterItem>() private val senderSpinnerList = ArrayList<SenderAdapterItem>()
private lateinit var senderSpinnerAdapter: SenderSpinnerAdapter<*> private lateinit var senderSpinnerAdapter: SenderSpinnerAdapter<*>
//已安装App信息列表 private var ruleId: Long = 0
private val appListSpinnerList = ArrayList<AppListAdapterItem>() private var ruleType: String = "app"
private lateinit var appListSpinnerAdapter: AppListSpinnerAdapter<*>
private val appListObserver = Observer { it: String ->
Log.d(TAG, "EVENT_LOAD_APP_LIST: $it")
initAppSpinner()
}
@JvmField
@AutoWired(name = KEY_RULE_ID)
var ruleId: Long = 0
@JvmField
@AutoWired(name = KEY_RULE_TYPE)
var ruleType: String = "sms"
@JvmField
@AutoWired(name = KEY_RULE_CLONE)
var isClone: Boolean = false
override fun initArgs() { override fun initArgs() {
XRouter.getInstance().inject(this) XRouter.getInstance().inject(this)
@ -101,8 +73,8 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
override fun viewBindingInflate( override fun viewBindingInflate(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup, container: ViewGroup,
): FragmentRulesEditBinding { ): FragmentTasksActionNotificationBinding {
return FragmentRulesEditBinding.inflate(inflater, container, false) return FragmentTasksActionNotificationBinding.inflate(inflater, container, false)
} }
override fun initTitle(): TitleBar? { override fun initTitle(): TitleBar? {
@ -115,74 +87,46 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
* 初始化控件 * 初始化控件
*/ */
override fun initViews() { override fun initViews() {
when (ruleType) { //测试按钮增加倒计时,避免重复点击
"app" -> { mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 3)
titleBar?.setTitle(R.string.app_rule) mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
binding!!.layoutSimSlot.visibility = View.GONE override fun onCountDown(time: Int) {
binding!!.rbPhone.visibility = View.GONE binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
binding!!.rbCallType.visibility = View.GONE
binding!!.rbContent.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_app_tips)
binding!!.btInsertExtra.visibility = View.GONE
binding!!.btInsertSender.visibility = View.GONE
binding!!.btInsertContent.visibility = View.GONE
//初始化APP下拉列表
initAppSpinner()
//监听已安装App信息列表加载完成事件
LiveEventBus.get(EVENT_LOAD_APP_LIST, String::class.java).observeStickyForever(appListObserver)
} }
"call" -> { override fun onFinished() {
titleBar?.setTitle(R.string.call_rule) binding!!.btnTest.text = getString(R.string.test)
binding!!.rbContent.visibility = View.GONE }
binding!!.rbPackageName.visibility = View.GONE })
binding!!.rbUid.visibility = View.GONE
binding!!.rbInformContent.visibility = View.GONE
//binding!!.rbMultiMatch.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_call_tips)
binding!!.btInsertContent.visibility = View.GONE
binding!!.btInsertSenderApp.visibility = View.GONE
binding!!.btInsertUid.visibility = View.GONE
binding!!.btInsertTitleApp.visibility = View.GONE
binding!!.btInsertContentApp.visibility = View.GONE
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出 Log.d(TAG, "initViews eventData:$eventData")
binding!!.spCallType.setItems(CALL_TYPE_MAP.values.toList()) if (eventData != null) {
binding!!.spCallType.setOnItemSelectedListener { _: MaterialSpinner?, _: Int, _: Long, item: Any -> val settingVo = Gson().fromJson(eventData, Rule::class.java)
CALL_TYPE_MAP.forEach { Log.d(TAG, "initViews settingVo:$settingVo")
if (it.value == item) callType = it.key.toInt()
} Log.d(TAG, settingVo.senderList.toString())
} settingVo.senderList.forEach {
binding!!.spCallType.setOnNothingSelectedListener { senderId = it.id
callType = 1 senderListSelected.add(it)
callTypeIndex = 0
binding!!.spCallType.selectedIndex = callTypeIndex
}
binding!!.spCallType.selectedIndex = callTypeIndex
} }
else -> { binding!!.rgSenderLogic.check(settingVo.getSenderLogicCheckId())
titleBar?.setTitle(R.string.sms_rule) if (!TextUtils.isEmpty(settingVo.smsTemplate.trim())) {
binding!!.rbCallType.visibility = View.GONE binding!!.sbSmsTemplate.isChecked = true
binding!!.rbPackageName.visibility = View.GONE binding!!.layoutSmsTemplate.visibility = View.VISIBLE
binding!!.rbUid.visibility = View.GONE binding!!.etSmsTemplate.setText(settingVo.smsTemplate.trim())
binding!!.rbInformContent.visibility = View.GONE
binding!!.btInsertSenderApp.visibility = View.GONE
binding!!.btInsertUid.visibility = View.GONE
binding!!.btInsertTitleApp.visibility = View.GONE
binding!!.btInsertContentApp.visibility = View.GONE
} }
if (!TextUtils.isEmpty(settingVo.regexReplace.trim())) {
binding!!.sbRegexReplace.isChecked = true
binding!!.layoutRegexReplace.visibility = View.VISIBLE
binding!!.etRegexReplace.setText(settingVo.regexReplace.trim())
}
silentPeriodStart = settingVo.silentPeriodStart
silentPeriodEnd = settingVo.silentPeriodEnd
} }
if (ruleId <= 0) { //新增 //初始化发送通道下拉框
titleBar?.setSubTitle(getString(R.string.add_rule)) initSenderSpinner()
binding!!.btnDel.setText(R.string.discard)
initSenderSpinner()
} else { //编辑 & 克隆
binding!!.btnDel.setText(R.string.del)
initForm()
}
} }
override fun initListeners() { override fun initListeners() {
@ -200,64 +144,8 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
binding!!.btnDel.setOnClickListener(this) binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this) binding!!.btnSave.setOnClickListener(this)
binding!!.sbStatus.setOnCheckedChangeListener(this)
binding!!.sbSmsTemplate.setOnCheckedChangeListener(this) binding!!.sbSmsTemplate.setOnCheckedChangeListener(this)
binding!!.sbRegexReplace.setOnCheckedChangeListener(this) binding!!.sbRegexReplace.setOnCheckedChangeListener(this)
binding!!.rgFiled.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
if (ruleType == "app" && appListSpinnerList.isNotEmpty()) {
binding!!.layoutAppList.visibility = if (checkedId == R.id.rb_inform_content) View.GONE else View.VISIBLE
}
when (checkedId) {
R.id.rb_transpond_all -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.GONE
}
R.id.rb_multi_match -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.VISIBLE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.VISIBLE
}
R.id.rb_call_type -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.GONE
binding!!.spCallType.visibility = View.VISIBLE
}
else -> {
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.VISIBLE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.VISIBLE
}
}
}
binding!!.rgCheck.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int ->
if (group != null && checkedId > 0) {
binding!!.rgCheck2.clearCheck()
group.check(checkedId)
}
}
binding!!.rgCheck2.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int ->
if (group != null && checkedId > 0) {
binding!!.rgCheck.clearCheck()
group.check(checkedId)
}
}
} }
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
@ -353,31 +241,46 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
} }
R.id.btn_test -> { R.id.btn_test -> {
val ruleNew = checkForm() val settingVo = checkSetting()
testRule(ruleNew) val from = "测试号码"
val content = "测试内容"
val simInfo = "SIM1_123456789"
val msgInfo = MsgInfo(ruleType, from, content, Date(), simInfo)
if (!settingVo.checkMsg(msgInfo)) {
throw Exception(getString(R.string.unmatched_rule))
}
Thread {
try {
SendUtils.sendMsgSender(msgInfo, settingVo)
} catch (e: Exception) {
e.printStackTrace()
LiveEventBus.get(EVENT_TOAST_ERROR, String::class.java).post(e.message.toString())
}
}.start()
return return
} }
R.id.btn_del -> { R.id.btn_del -> {
if (ruleId <= 0 || isClone) { popToBack()
popToBack()
return
}
MaterialDialog.Builder(requireContext()).title(R.string.delete_rule_title).content(R.string.delete_rule_tips).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(ruleId)
XToastUtils.success(R.string.delete_rule_toast)
popToBack()
}.show()
return return
} }
R.id.btn_save -> { R.id.btn_save -> {
val ruleNew = checkForm() val settingVo = checkSetting()
if (isClone) ruleNew.id = 0 var description = getString(R.string.select_sender) + ": "
Log.d(TAG, ruleNew.toString()) description += settingVo.senderList.joinToString(",") { it.name }
viewModel.insertOrUpdate(ruleNew) if (settingVo.senderList.size > 1) {
XToastUtils.success(R.string.tipSaveSuccess) description += "; " + getString(R.string.sender_logic) + ": " + when (settingVo.senderLogic) {
SENDER_LOGIC_UNTIL_FAIL -> getString(R.string.sender_logic_until_fail)
SENDER_LOGIC_UNTIL_SUCCESS -> getString(R.string.sender_logic_until_success)
else -> getString(R.string.sender_logic_all)
}
}
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_NOTIFICATION, intent)
popToBack() popToBack()
return return
} }
@ -511,148 +414,23 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
} }
} }
//初始化APP下拉列表
private fun initAppSpinner() {
if (ruleType != "app") return
//未开启异步获取已安装App信息开关时规则编辑不显示已安装APP下拉框
if (!SettingUtils.enableLoadUserAppList && !SettingUtils.enableLoadSystemAppList) return
if (App.UserAppList.isEmpty() && App.SystemAppList.isEmpty()) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(XUtil.getContext()).enqueue(request)
return
}
appListSpinnerList.clear()
if (SettingUtils.enableLoadUserAppList) {
for (appInfo in App.UserAppList) {
if (TextUtils.isEmpty(appInfo.packageName)) continue
appListSpinnerList.add(AppListAdapterItem(appInfo.name, appInfo.icon, appInfo.packageName))
}
}
if (SettingUtils.enableLoadSystemAppList) {
for (appInfo in App.SystemAppList) {
if (TextUtils.isEmpty(appInfo.packageName)) continue
appListSpinnerList.add(AppListAdapterItem(appInfo.name, appInfo.icon, appInfo.packageName))
}
}
//列表为空也不显示下拉框
if (appListSpinnerList.isEmpty()) return
appListSpinnerAdapter = AppListSpinnerAdapter(appListSpinnerList).setIsFilterKey(true).setFilterColor("#EF5362").setBackgroundSelector(R.drawable.selector_custom_spinner_bg)
binding!!.spApp.setAdapter(appListSpinnerAdapter)
binding!!.spApp.setOnItemClickListener { _: AdapterView<*>, _: View, position: Int, _: Long ->
try {
val appInfo = appListSpinnerAdapter.getItemSource(position) as AppListAdapterItem
CommonUtils.insertOrReplaceText2Cursor(binding!!.etValue, appInfo.packageName.toString())
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
}
}
binding!!.layoutAppList.visibility = View.VISIBLE
}
//初始化表单
private fun initForm() {
AppDatabase.getInstance(requireContext()).ruleDao().get(ruleId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver<Rule> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onSuccess(rule: Rule) {
Log.d(TAG, rule.senderList.toString())
rule.senderList.forEach {
senderId = it.id
senderListSelected.add(it)
}
if (isClone) {
titleBar?.setSubTitle(getString(R.string.clone_rule))
binding!!.btnDel.setText(R.string.discard)
} else {
titleBar?.setSubTitle(getString(R.string.edit_rule))
}
Log.d(TAG, rule.toString())
binding!!.rgSenderLogic.check(rule.getSenderLogicCheckId())
binding!!.rgSimSlot.check(rule.getSimSlotCheckId())
binding!!.rgFiled.check(rule.getFiledCheckId())
val checkId = rule.getCheckCheckId()
if (checkId == R.id.rb_is || checkId == R.id.rb_contain || checkId == R.id.rb_not_contain) {
binding!!.rgCheck.check(checkId)
} else {
binding!!.rgCheck2.check(checkId)
}
binding!!.etValue.setText(rule.value)
if (ruleType == "call" && rule.filed == FILED_CALL_TYPE) {
callType = rule.value.toInt()
callTypeIndex = callType - 1
binding!!.spCallType.selectedIndex = callTypeIndex
}
binding!!.sbSmsTemplate.isChecked = !TextUtils.isEmpty(rule.smsTemplate.trim())
binding!!.etSmsTemplate.setText(rule.smsTemplate.trim())
binding!!.sbRegexReplace.isChecked = !TextUtils.isEmpty(rule.regexReplace.trim())
binding!!.etRegexReplace.setText(rule.regexReplace.trim())
binding!!.sbStatus.isChecked = rule.statusChecked
silentPeriodStart = rule.silentPeriodStart
silentPeriodEnd = rule.silentPeriodEnd
//初始化发送通道下拉框
initSenderSpinner()
}
})
}
//提交前检查表单 //提交前检查表单
private fun checkForm(): Rule { private fun checkSetting(): Rule {
if (senderListSelected.isEmpty() || senderId == 0L) { if (senderListSelected.isEmpty() || senderId == 0L) {
throw Exception(getString(R.string.new_sender_first)) throw Exception(getString(R.string.new_sender_first))
} }
val filed = when (binding!!.rgFiled.checkedRadioButtonId) {
R.id.rb_content -> FILED_MSG_CONTENT
R.id.rb_phone -> FILED_PHONE_NUM
R.id.rb_call_type -> FILED_CALL_TYPE
R.id.rb_package_name -> FILED_PACKAGE_NAME
R.id.rb_uid -> FILED_UID
R.id.rb_inform_content -> FILED_INFORM_CONTENT
R.id.rb_multi_match -> FILED_MULTI_MATCH
else -> FILED_TRANSPOND_ALL
}
val check = when (kotlin.math.max(binding!!.rgCheck.checkedRadioButtonId, binding!!.rgCheck2.checkedRadioButtonId)) {
R.id.rb_contain -> CHECK_CONTAIN
R.id.rb_not_contain -> CHECK_NOT_CONTAIN
R.id.rb_start_with -> CHECK_START_WITH
R.id.rb_end_with -> CHECK_END_WITH
R.id.rb_regex -> CHECK_REGEX
else -> CHECK_IS
}
var value = binding!!.etValue.text.toString().trim()
if (FILED_CALL_TYPE == filed) {
value = callType.toString()
if (callType !in 1..6) {
throw Exception(getString(R.string.invalid_call_type))
}
} else if (FILED_TRANSPOND_ALL != filed && TextUtils.isEmpty(value)) {
throw Exception(getString(R.string.invalid_match_value))
}
if (FILED_MULTI_MATCH == filed) {
val lineError = checkMultiMatch(value)
if (lineError > 0) {
throw Exception(String.format(getString(R.string.invalid_multi_match), lineError))
}
}
val filed = FILED_TRANSPOND_ALL
val check = CHECK_IS
val value = ""
val smsTemplate = binding!!.etSmsTemplate.text.toString().trim() val smsTemplate = binding!!.etSmsTemplate.text.toString().trim()
val regexReplace = binding!!.etRegexReplace.text.toString().trim() val regexReplace = binding!!.etRegexReplace.text.toString().trim()
val lineNum = checkRegexReplace(regexReplace) val lineNum = checkRegexReplace(regexReplace)
if (lineNum > 0) { if (lineNum > 0) {
throw Exception(String.format(getString(R.string.invalid_regex_replace), lineNum)) throw Exception(String.format(getString(R.string.invalid_regex_replace), lineNum))
} }
val simSlot = CHECK_SIM_SLOT_ALL
val status = STATUS_ON
val senderLogic = when (binding!!.rgSenderLogic.checkedRadioButtonId) { val senderLogic = when (binding!!.rgSenderLogic.checkedRadioButtonId) {
R.id.rb_sender_logic_until_fail -> SENDER_LOGIC_UNTIL_FAIL R.id.rb_sender_logic_until_fail -> SENDER_LOGIC_UNTIL_FAIL
@ -660,15 +438,6 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
else -> SENDER_LOGIC_ALL else -> SENDER_LOGIC_ALL
} }
val simSlot = when (binding!!.rgSimSlot.checkedRadioButtonId) {
R.id.rb_sim_slot_1 -> CHECK_SIM_SLOT_1
R.id.rb_sim_slot_2 -> CHECK_SIM_SLOT_2
else -> CHECK_SIM_SLOT_ALL
}
val status = if (binding!!.sbStatus.isChecked) STATUS_ON else STATUS_OFF
//if (status == STATUS_OFF) {
// throw Exception(getString(R.string.invalid_rule_status))
//}
return Rule( return Rule(
ruleId, ruleId,
ruleType, ruleType,
@ -688,23 +457,6 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
) )
} }
//检查多重匹配规则是否正确
private fun checkMultiMatch(ruleStr: String?): Int {
if (TextUtils.isEmpty(ruleStr)) return 0
//Log.d(TAG, getString(R.string.regex_multi_match))
val regex = Regex(pattern = getString(R.string.regex_multi_match))
var lineNum = 1
val lineArray = ruleStr?.split("\\n".toRegex())?.toTypedArray()
for (line in lineArray!!) {
Log.d(TAG, line)
if (!line.matches(regex)) return lineNum
lineNum++
}
return 0
}
//检查正则替换填写是否正确 //检查正则替换填写是否正确
private fun checkRegexReplace(regexReplace: String?): Int { private fun checkRegexReplace(regexReplace: String?): Int {
if (TextUtils.isEmpty(regexReplace)) return 0 if (TextUtils.isEmpty(regexReplace)) return 0
@ -719,109 +471,4 @@ class NotificationFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnC
return 0 return 0
} }
private fun testRule(rule: Rule) {
val dialogTest = View.inflate(requireContext(), R.layout.dialog_rule_test, null)
val tvSimSlot = dialogTest.findViewById<TextView>(R.id.tv_sim_slot)
val rgSimSlot = dialogTest.findViewById<RadioGroup>(R.id.rg_sim_slot)
val tvFrom = dialogTest.findViewById<TextView>(R.id.tv_from)
val etFrom = dialogTest.findViewById<EditText>(R.id.et_from)
val tvTitle = dialogTest.findViewById<TextView>(R.id.tv_title)
val etTitle = dialogTest.findViewById<EditText>(R.id.et_title)
val tvContent = dialogTest.findViewById<TextView>(R.id.tv_content)
val etContent = dialogTest.findViewById<EditText>(R.id.et_content)
//通话类型
val tvCallType = dialogTest.findViewById<TextView>(R.id.tv_call_type)
val spCallType = dialogTest.findViewById<MaterialSpinner>(R.id.sp_call_type)
var callTypeTest = callType
var callTypeIndexTest = callTypeIndex
if ("app" == ruleType) {
tvSimSlot.visibility = View.GONE
rgSimSlot.visibility = View.GONE
tvTitle.visibility = View.VISIBLE
etTitle.visibility = View.VISIBLE
tvFrom.setText(R.string.test_package_name)
tvContent.setText(R.string.test_inform_content)
tvCallType.visibility = View.GONE
spCallType.visibility = View.GONE
} else if ("call" == ruleType) {
tvContent.visibility = View.GONE
etContent.visibility = View.GONE
tvCallType.visibility = View.VISIBLE
spCallType.visibility = View.VISIBLE
spCallType.setItems(CALL_TYPE_MAP.values.toList())
spCallType.setOnItemSelectedListener { _: MaterialSpinner?, _: Int, _: Long, item: Any ->
CALL_TYPE_MAP.forEach {
if (it.value == item) callTypeTest = it.key.toInt()
}
}
spCallType.setOnNothingSelectedListener {
callTypeTest = callType
callTypeIndexTest = callTypeIndex
spCallType.selectedIndex = callTypeIndexTest
}
spCallType.selectedIndex = callTypeIndexTest
}
MaterialDialog.Builder(requireContext()).iconRes(android.R.drawable.ic_dialog_email).title(R.string.rule_tester).customView(dialogTest, true).cancelable(false).autoDismiss(false).neutralText(R.string.action_back).neutralColor(ResUtils.getColors(R.color.darkGrey)).onNeutral { dialog: MaterialDialog?, _: DialogAction? ->
dialog?.dismiss()
}.positiveText(R.string.action_test).onPositive { _: MaterialDialog?, _: DialogAction? ->
try {
val simSlot = when (if (ruleType == "app") -1 else rgSimSlot.checkedRadioButtonId) {
R.id.rb_sim_slot_1 -> 0
R.id.rb_sim_slot_2 -> 1
else -> -1
}
val testSim = "SIM" + (simSlot + 1)
val ruleSim: String = rule.simSlot
if (ruleSim != "ALL" && ruleSim != testSim) {
throw Exception(getString(R.string.card_slot_does_not_match))
}
//获取卡槽信息
val simInfo = when (simSlot) {
0 -> "SIM1_" + SettingUtils.extraSim1
1 -> "SIM2_" + SettingUtils.extraSim2
else -> etTitle.text.toString()
}
val subId = when (simSlot) {
0 -> SettingUtils.subidSim1
1 -> SettingUtils.subidSim2
else -> 0
}
val msg = StringBuilder()
if (ruleType == "call") {
val phoneNumber = etFrom.text.toString()
val contacts = PhoneUtils.getContactByNumber(phoneNumber)
val contactName = if (contacts.isNotEmpty()) contacts[0].name else ResUtils.getString(R.string.unknown_number)
msg.append(ResUtils.getString(R.string.contact)).append(contactName).append("\n")
msg.append(ResUtils.getString(R.string.mandatory_type))
msg.append(CALL_TYPE_MAP[callType.toString()] ?: ResUtils.getString(R.string.unknown_call))
} else {
msg.append(etContent.text.toString())
}
val msgInfo = MsgInfo(ruleType, etFrom.text.toString(), msg.toString(), Date(), simInfo, simSlot, subId, callTypeTest)
if (!rule.checkMsg(msgInfo)) {
throw Exception(getString(R.string.unmatched_rule))
}
Thread {
try {
SendUtils.sendMsgSender(msgInfo, rule)
} catch (e: Exception) {
e.printStackTrace()
if (Looper.myLooper() == null) Looper.prepare()
XToastUtils.error(e.message.toString())
Looper.loop()
}
}.start()
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
}
}.show()
}
} }

View File

@ -24,6 +24,7 @@ import com.idormy.sms.forwarder.utils.EVENT_KEY_PHONE_NUMBERS
import com.idormy.sms.forwarder.utils.EVENT_KEY_SIM_SLOT import com.idormy.sms.forwarder.utils.EVENT_KEY_SIM_SLOT
import com.idormy.sms.forwarder.utils.HttpServerUtils import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_TEST_ACTION import com.idormy.sms.forwarder.utils.KEY_TEST_ACTION
import com.idormy.sms.forwarder.utils.PhoneUtils import com.idormy.sms.forwarder.utils.PhoneUtils
@ -191,6 +192,7 @@ class SendSmsFragment : BaseFragment<FragmentTasksActionSendSmsBinding?>(), View
R.id.btn_save -> { R.id.btn_save -> {
val settingVo = checkSetting() val settingVo = checkSetting()
val intent = Intent() val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo)) intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_SENDSMS, intent) setFragmentResult(TASK_ACTION_SENDSMS, intent)
popToBack() popToBack()

View File

@ -10,9 +10,14 @@ import androidx.work.WorkerParameters
import com.google.gson.Gson import com.google.gson.Gson
import com.idormy.sms.forwarder.App import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.task.SmsSetting import com.idormy.sms.forwarder.entity.task.SmsSetting
import com.idormy.sms.forwarder.utils.PhoneUtils import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.TASK_ACTION_NOTIFICATION
import com.idormy.sms.forwarder.utils.TASK_ACTION_SENDSMS import com.idormy.sms.forwarder.utils.TASK_ACTION_SENDSMS
import com.idormy.sms.forwarder.utils.Worker
import com.xuexiang.xui.utils.ResUtils import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xutil.XUtil import com.xuexiang.xutil.XUtil
@ -22,14 +27,16 @@ class ActionWorker(context: Context, params: WorkerParameters) : CoroutineWorker
private val TAG: String = ActionWorker::class.java.simpleName private val TAG: String = ActionWorker::class.java.simpleName
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val taskId = inputData.getLong("taskId", -1L) val taskId = inputData.getLong(Worker.taskId, -1L)
val actionType = inputData.getInt("actionType", -1) val actionType = inputData.getInt(Worker.actionType, -1)
val actionSetting = inputData.getString("actionSetting") val actionSetting = inputData.getString(Worker.actionSetting)
Log.d(TAG, "taskId: $taskId, actionSetting: $actionSetting") val msgInfoJson = inputData.getString(Worker.sendMsgInfo)
Log.d(TAG, "taskId: $taskId, actionType: $actionType, actionSetting: $actionSetting, msgInfoJson: $msgInfoJson")
if (taskId == -1L || actionSetting == null) { if (taskId == -1L || actionSetting == null) {
Log.d(TAG, "taskId is -1L or actionSetting is null") Log.d(TAG, "taskId is -1L or actionSetting is null")
return Result.failure() return Result.failure()
} }
val msgInfo = Gson().fromJson(msgInfoJson, MsgInfo::class.java)
when (actionType) { when (actionType) {
TASK_ACTION_SENDSMS -> { TASK_ACTION_SENDSMS -> {
@ -59,6 +66,18 @@ class ActionWorker(context: Context, params: WorkerParameters) : CoroutineWorker
return Result.success() return Result.success()
} }
TASK_ACTION_NOTIFICATION -> {
return try {
val settingVo = Gson().fromJson(actionSetting, Rule::class.java)
//自动任务的不需要吐司或者更新日志,特殊处理 logId = -1msgId = -1
SendUtils.sendMsgSender(msgInfo, settingVo, 0, -1L, -1L)
Result.success()
} catch (e: Exception) {
e.printStackTrace()
Result.failure()
}
}
else -> { else -> {
Log.d(TAG, "任务$taskIdaction.type is $actionType") Log.d(TAG, "任务$taskIdaction.type is $actionType")
return Result.failure() return Result.failure()

View File

@ -10,8 +10,10 @@ import androidx.work.WorkerParameters
import com.google.gson.Gson import com.google.gson.Gson
import com.idormy.sms.forwarder.App import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.database.AppDatabase import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.task.CronSetting import com.idormy.sms.forwarder.entity.task.CronSetting
import com.idormy.sms.forwarder.entity.task.TaskSetting import com.idormy.sms.forwarder.entity.task.TaskSetting
import com.idormy.sms.forwarder.utils.Worker
import com.idormy.sms.forwarder.utils.task.CronJobScheduler import com.idormy.sms.forwarder.utils.task.CronJobScheduler
import gatewayapps.crondroid.CronExpression import gatewayapps.crondroid.CronExpression
import java.util.Date import java.util.Date
@ -80,6 +82,9 @@ class CronWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
return Result.success() return Result.success()
} }
//组装消息体
val msgInfo = MsgInfo("task", task.name, task.description, Date(), task.name)
// TODO: 执行具体任务 // TODO: 执行具体任务
val actionList = Gson().fromJson(task.actions, Array<TaskSetting>::class.java).toMutableList() val actionList = Gson().fromJson(task.actions, Array<TaskSetting>::class.java).toMutableList()
if (actionList.isEmpty()) { if (actionList.isEmpty()) {
@ -88,9 +93,10 @@ class CronWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
} }
for (action in actionList) { for (action in actionList) {
val actionData = Data.Builder() val actionData = Data.Builder()
.putLong("taskId", task.id) .putLong(Worker.taskId, task.id)
.putInt("actionType", action.type) .putInt(Worker.actionType, action.type)
.putString("actionSetting", action.setting) .putString(Worker.actionSetting, action.setting)
.putString(Worker.sendMsgInfo, Gson().toJson(msgInfo))
.build() .build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build() val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest) WorkManager.getInstance().enqueue(actionRequest)

View File

@ -42,6 +42,9 @@
android:id="@+id/tv_description" android:id="@+id/tv_description"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start"
android:maxLines="2"
android:text="@string/add_action_tips" android:text="@string/add_action_tips"
android:textSize="12sp" /> android:textSize="12sp" />

View File

@ -0,0 +1,355 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/xui_config_color_background"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical">
<LinearLayout
style="@style/ruleBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_sender"
android:textStyle="bold" />
<com.xuexiang.xui.widget.spinner.editspinner.EditSpinner
android:id="@+id/sp_sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
app:es_hint="@string/choose_sender"
app:es_maxLength="20"
app:es_maxLine="1" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/xui_config_color_separator_light" />
<LinearLayout
android:id="@+id/layout_Senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<LinearLayout
android:id="@+id/layout_sender_logic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sender_logic"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_sender_logic"
style="@style/rg_style"
android:layout_marginStart="5dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_sender_logic_all"
style="@style/rg_rb_style_wrap"
android:checked="true"
android:text="@string/sender_logic_all" />
<RadioButton
android:id="@+id/rb_sender_logic_until_fail"
style="@style/rg_rb_style_wrap"
android:text="@string/sender_logic_until_fail" />
<RadioButton
android:id="@+id/rb_sender_logic_until_success"
style="@style/rg_rb_style_wrap"
android:text="@string/sender_logic_until_success" />
</RadioGroup>
</LinearLayout>
</LinearLayout>
<LinearLayout
style="@style/ruleBarStyleWithSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="@string/enable_custom_templates"
android:textStyle="bold" />
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_sms_template"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="DuplicateSpeakableTextCheck,TouchTargetSizeCheck" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_sms_template"
style="@style/ruleBarStyleNoMarginTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_custom_templates_tips"
android:textSize="11sp" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_sms_template"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
app:met_clearButton="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_sender"
style="@style/insertButtonStyle"
android:text="@string/insert_sender" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_content"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_content" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_extra"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_extra" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_sender_app"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_sender_app" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_uid"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_uid" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_title_app"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_title_app" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_content_app"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_content_app" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_time"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_time" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/bt_insert_device_name"
style="@style/insertButtonStyle"
android:layout_marginStart="5dp"
android:text="@string/insert_device_name" />
</LinearLayout>
</LinearLayout>
<LinearLayout
style="@style/ruleBarStyleWithSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="@string/enable_regex_replace"
android:textStyle="bold" />
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_regex_replace"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="TouchTargetSizeCheck" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_regex_replace"
style="@style/ruleBarStyleNoMarginTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_regex_replace_tips"
android:textSize="11sp" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_regex_replace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_silent_period"
style="@style/ruleBarStyleWithSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="15dp"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="180dp"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="TextSizeCheck">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/silent_time_period"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/silent_time_period_tips"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<TextView
android:id="@+id/tv_silent_period"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:gravity="center" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_silent_period"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="5dp"
android:text="@string/select"
android:textColor="@color/white"
android:textSize="10sp"
app:sb_color_unpressed="@color/colorPrimary"
app:sb_ripple_color="@color/white"
app:sb_ripple_duration="500"
app:sb_shape_type="rectangle"
tools:ignore="SmallSp,TextContrastCheck,TouchTargetSizeCheck" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="10dp">
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_del"
style="@style/SuperButton.Gray.Icon"
android:drawableStart="@drawable/icon_delete"
android:paddingStart="15dp"
android:text="@string/del"
android:textSize="11sp"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_save"
style="@style/SuperButton.Blue.Icon"
android:layout_marginStart="10dp"
android:drawableStart="@drawable/icon_save"
android:paddingStart="15dp"
android:text="@string/save"
android:textSize="11sp"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_test"
style="@style/SuperButton.Green.Icon"
android:layout_marginStart="10dp"
android:drawableStart="@drawable/icon_test"
android:paddingStart="15dp"
android:text="@string/test"
android:textSize="11sp"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
</LinearLayout>
</LinearLayout>