优化:单个转发规则支持绑定多个发送通道,且支持执行逻辑(全部执行/失败即止/成功即止) #247

优化:转发日志列表以原始信息为主,聚合展示转发日志(一对多)
pull/286/head
pppscn 2 years ago
parent b4870207d1
commit b79d3d8493

@ -17,10 +17,7 @@ import com.gyf.cactus.ext.cactus
import com.idormy.sms.forwarder.activity.MainActivity
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.database.repository.*
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.receiver.CactusReceiver
import com.idormy.sms.forwarder.service.BatteryService
@ -48,6 +45,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
val applicationScope = CoroutineScope(SupervisorJob())
val database by lazy { AppDatabase.getInstance(this) }
val frpcRepository by lazy { FrpcRepository(database.frpcDao()) }
val msgRepository by lazy { MsgRepository(database.msgDao()) }
val logsRepository by lazy { LogsRepository(database.logsDao()) }
val ruleRepository by lazy { RuleRepository(database.ruleDao()) }
val senderRepository by lazy { SenderRepository(database.senderDao()) }

@ -1,434 +1,434 @@
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.cactus.ext.cactusUpdateNotification
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.WidgetItemAdapter
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.databinding.ActivityMainBinding
import com.idormy.sms.forwarder.fragment.*
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTips
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTipsForce
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.callback.DownloadProgressCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xui.adapter.FragmentAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.GravityEnum
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.file.FileUtils
import com.xuexiang.xutil.net.NetworkUtils
import frpclib.Frpclib
import io.reactivex.CompletableObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.io.File
@Suppress("DEPRECATION", "PrivatePropertyName")
class MainActivity : BaseActivity<ActivityMainBinding?>(),
View.OnClickListener,
BottomNavigationView.OnNavigationItemSelectedListener,
Toolbar.OnMenuItemClickListener,
RecyclerViewHolder.OnItemClickListener<PageInfo> {
private val TAG: String = MainActivity::class.java.simpleName
private lateinit var mTitles: Array<String>
private var logsType: String = "sms"
private var ruleType: String = "sms"
override fun viewBindingInflate(inflater: LayoutInflater?): ActivityMainBinding {
return ActivityMainBinding.inflate(inflater!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
initData()
initListeners()
//不在最近任务列表中显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SettingUtils.enableExcludeFromRecents) {
val am = App.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.let {
val tasks = it.appTasks
if (!tasks.isNullOrEmpty()) {
tasks[0].setExcludeFromRecents(true)
}
}
}
}
override val isSupportSlideBack: Boolean
get() = false
private fun initViews() {
WidgetUtils.clearActivityBackground(this)
mTitles = ResUtils.getStringArray(R.array.home_titles)
binding!!.includeMain.toolbar.title = mTitles[0]
binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
binding!!.includeMain.toolbar.setOnMenuItemClickListener(this)
//主页内容填充
val fragments = arrayOf(
LogsFragment(),
RulesFragment(),
SendersFragment(),
SettingsFragment()
)
val adapter = FragmentAdapter(supportFragmentManager, fragments)
binding!!.includeMain.viewPager.offscreenPageLimit = mTitles.size - 1
binding!!.includeMain.viewPager.adapter = adapter
if (!SettingUtils.enableHelpTip) {
val headerView = binding!!.navView.getHeaderView(0)
val tvSlogan = headerView.findViewById<TextView>(R.id.tv_slogan)
tvSlogan.visibility = View.GONE
}
}
private fun initData() {
//仅当有WIFI网络时自动检查更新/获取提示
if (NetworkUtils.isWifi() && NetworkUtils.isHaveInternet()) {
showTips(this)
XUpdateInit.checkUpdate(this, false)
}
}
fun initListeners() {
val toggle = ActionBarDrawerToggle(
this,
binding!!.drawerLayout,
binding!!.includeMain.toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
binding!!.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
//侧边栏点击事件
binding!!.navView.setNavigationItemSelectedListener { menuItem: MenuItem ->
if (menuItem.isCheckable) {
binding!!.drawerLayout.closeDrawers()
return@setNavigationItemSelectedListener handleNavigationItemSelected(menuItem)
} else {
when (menuItem.itemId) {
R.id.nav_server -> openNewPage(ServerFragment::class.java)
R.id.nav_client -> openNewPage(ClientFragment::class.java)
R.id.nav_frpc -> {
if (!FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
MaterialDialog.Builder(this)
.title(
String.format(
getString(R.string.frpclib_download_title),
FRPC_LIB_VERSION
)
)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
return@setNavigationItemSelectedListener false
}
if (FRPC_LIB_VERSION == Frpclib.getVersion()) {
openNewPage(FrpcFragment::class.java)
} else {
MaterialDialog.Builder(this)
.title(R.string.frpclib_version_mismatch)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
}
}
R.id.nav_app_list -> openNewPage(AppListFragment::class.java)
R.id.nav_logcat -> openNewPage(LogcatFragment::class.java)
R.id.nav_help -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
R.id.nav_about -> openNewPage(AboutFragment::class.java)
else -> XToastUtils.toast("Click:" + menuItem.title)
}
}
true
}
//主页事件监听
binding!!.includeMain.viewPager.addOnPageChangeListener(object :
ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
}
override fun onPageSelected(position: Int) {
val item = binding!!.includeMain.bottomNavigation.menu.getItem(position)
binding!!.includeMain.toolbar.title = item.title
binding!!.includeMain.toolbar.menu.clear()
when (item.title) {
getString(R.string.menu_rules) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_rules
)
getString(R.string.menu_senders) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_senders
)
getString(R.string.menu_settings) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_settings
)
else -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
}
item.isChecked = true
updateSideNavStatus(item)
}
override fun onPageScrollStateChanged(state: Int) {}
})
binding!!.includeMain.bottomNavigation.setOnNavigationItemSelectedListener(this)
//tabBar分类切换
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).observe(this) { type: String ->
logsType = type
}
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).observe(this) { type: String ->
ruleType = type
}
//更新通知栏文案
LiveEventBus.get(EVENT_UPDATE_NOTIFY, String::class.java).observe(this) { notify: String ->
cactusUpdateNotification {
setContent(notify)
}
}
}
/**
* 处理侧边栏点击事件
*
* @param menuItem
* @return
*/
private fun handleNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
return true
}
}
return false
}
@SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_notifications -> {
showTipsForce(this)
}
R.id.action_clear_logs -> {
MaterialDialog.Builder(this)
.content(R.string.delete_type_log_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
AppDatabase.getInstance(this)
.logsDao()
.deleteAll(logsType)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : CompletableObserver {
override fun onSubscribe(d: Disposable) {}
override fun onComplete() {
XToastUtils.success(R.string.delete_type_log_toast)
}
override fun onError(e: Throwable) {
e.message?.let { XToastUtils.error(it) }
}
})
}
.show()
}
R.id.action_add_sender -> {
val dialog = BottomSheetDialog(this)
val view: View =
LayoutInflater.from(this).inflate(R.layout.dialog_sender_bottom_sheet, null)
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f))
val widgetItemAdapter = WidgetItemAdapter(SENDER_FRAGMENT_LIST)
widgetItemAdapter.setOnItemClickListener(this)
recyclerView.adapter = widgetItemAdapter
dialog.setContentView(view)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
dialog.show()
WidgetUtils.transparentBottomSheetDialogBackground(dialog)
}
R.id.action_add_rule -> {
PageOption.to(RulesEditFragment::class.java)
.putString(KEY_RULE_TYPE, ruleType)
.setNewActivity(true)
.open(this)
}
/*R.id.action_restore_settings -> {
XToastUtils.success(logsType)
}*/
}
return false
}
@SingleClick
override fun onClick(v: View) {
}
//================Navigation================//
/**
* 底部导航栏点击事件
*
* @param menuItem
* @return
*/
override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
updateSideNavStatus(menuItem)
return true
}
}
return false
}
/**
* 更新侧边栏菜单选中状态
*
* @param menuItem
*/
private fun updateSideNavStatus(menuItem: MenuItem) {
val side = binding!!.navView.menu.findItem(menuItem.itemId)
if (side != null) {
side.isChecked = true
}
}
//按返回键不退出回到桌面
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_HOME)
startActivity(intent)
}
@SingleClick
override fun onItemClick(itemView: View, widgetInfo: PageInfo, pos: Int) {
try {
@Suppress("UNCHECKED_CAST")
PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setNewActivity(true)
.putInt(KEY_SENDER_TYPE, pos) //注意:目前刚好是这个顺序而已
.open(this)
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(e.message.toString())
}
}
//动态加载FrpcLib
private fun downloadFrpcLib() {
val cpuAbi = when (Build.CPU_ABI) {
"x86" -> "x86"
"x86_64" -> "x86_64"
"arm64-v8a" -> "arm64-v8a"
else -> "armeabi-v7a"
}
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)
if (!soFile.exists()) soFile.mkdirs()
val downloadUrl = String.format(FRPC_LIB_DOWNLOAD_URL, FRPC_LIB_VERSION, cpuAbi)
val mContext = this
val dialog: MaterialDialog = MaterialDialog.Builder(mContext)
.title(String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION))
.content(getString(R.string.frpclib_download_content))
.contentGravity(GravityEnum.CENTER)
.progress(false, 0, true)
.progressNumberFormat("%2dMB/%1dMB")
.build()
XHttp.downLoad(downloadUrl)
.savePath(cacheDir.absolutePath)
.execute(object : DownloadProgressCallBack<String?>() {
override fun onStart() {
dialog.show()
}
override fun onError(e: ApiException) {
dialog.dismiss()
XToastUtils.error(e.message.toString())
}
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
Log.d(TAG, "onProgress: bytesRead=$bytesRead, contentLength=$contentLength")
dialog.maxProgress = (contentLength / 1048576L).toInt()
dialog.setProgress((bytesRead / 1048576L).toInt())
}
override fun onComplete(srcPath: String) {
dialog.dismiss()
Log.d(TAG, "srcPath = $srcPath")
val srcFile = File(srcPath)
val destFile = File("$libPath/libgojni.so")
FileUtils.moveFile(srcFile, destFile, null)
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid()) //杀掉以前进程
}
})
}
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.cactus.ext.cactusUpdateNotification
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.WidgetItemAdapter
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.databinding.ActivityMainBinding
import com.idormy.sms.forwarder.fragment.*
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTips
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTipsForce
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.callback.DownloadProgressCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xui.adapter.FragmentAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.GravityEnum
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.file.FileUtils
import com.xuexiang.xutil.net.NetworkUtils
import frpclib.Frpclib
import io.reactivex.CompletableObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.io.File
@Suppress("DEPRECATION", "PrivatePropertyName")
class MainActivity : BaseActivity<ActivityMainBinding?>(),
View.OnClickListener,
BottomNavigationView.OnNavigationItemSelectedListener,
Toolbar.OnMenuItemClickListener,
RecyclerViewHolder.OnItemClickListener<PageInfo> {
private val TAG: String = MainActivity::class.java.simpleName
private lateinit var mTitles: Array<String>
private var logsType: String = "sms"
private var ruleType: String = "sms"
override fun viewBindingInflate(inflater: LayoutInflater?): ActivityMainBinding {
return ActivityMainBinding.inflate(inflater!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
initData()
initListeners()
//不在最近任务列表中显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SettingUtils.enableExcludeFromRecents) {
val am = App.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.let {
val tasks = it.appTasks
if (!tasks.isNullOrEmpty()) {
tasks[0].setExcludeFromRecents(true)
}
}
}
}
override val isSupportSlideBack: Boolean
get() = false
private fun initViews() {
WidgetUtils.clearActivityBackground(this)
mTitles = ResUtils.getStringArray(R.array.home_titles)
binding!!.includeMain.toolbar.title = mTitles[0]
binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
binding!!.includeMain.toolbar.setOnMenuItemClickListener(this)
//主页内容填充
val fragments = arrayOf(
LogsFragment(),
RulesFragment(),
SendersFragment(),
SettingsFragment()
)
val adapter = FragmentAdapter(supportFragmentManager, fragments)
binding!!.includeMain.viewPager.offscreenPageLimit = mTitles.size - 1
binding!!.includeMain.viewPager.adapter = adapter
if (!SettingUtils.enableHelpTip) {
val headerView = binding!!.navView.getHeaderView(0)
val tvSlogan = headerView.findViewById<TextView>(R.id.tv_slogan)
tvSlogan.visibility = View.GONE
}
}
private fun initData() {
//仅当有WIFI网络时自动检查更新/获取提示
if (NetworkUtils.isWifi() && NetworkUtils.isHaveInternet()) {
showTips(this)
XUpdateInit.checkUpdate(this, false)
}
}
fun initListeners() {
val toggle = ActionBarDrawerToggle(
this,
binding!!.drawerLayout,
binding!!.includeMain.toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
binding!!.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
//侧边栏点击事件
binding!!.navView.setNavigationItemSelectedListener { menuItem: MenuItem ->
if (menuItem.isCheckable) {
binding!!.drawerLayout.closeDrawers()
return@setNavigationItemSelectedListener handleNavigationItemSelected(menuItem)
} else {
when (menuItem.itemId) {
R.id.nav_server -> openNewPage(ServerFragment::class.java)
R.id.nav_client -> openNewPage(ClientFragment::class.java)
R.id.nav_frpc -> {
if (!FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
MaterialDialog.Builder(this)
.title(
String.format(
getString(R.string.frpclib_download_title),
FRPC_LIB_VERSION
)
)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
return@setNavigationItemSelectedListener false
}
if (FRPC_LIB_VERSION == Frpclib.getVersion()) {
openNewPage(FrpcFragment::class.java)
} else {
MaterialDialog.Builder(this)
.title(R.string.frpclib_version_mismatch)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
}
}
R.id.nav_app_list -> openNewPage(AppListFragment::class.java)
R.id.nav_logcat -> openNewPage(LogcatFragment::class.java)
R.id.nav_help -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
R.id.nav_about -> openNewPage(AboutFragment::class.java)
else -> XToastUtils.toast("Click:" + menuItem.title)
}
}
true
}
//主页事件监听
binding!!.includeMain.viewPager.addOnPageChangeListener(object :
ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
}
override fun onPageSelected(position: Int) {
val item = binding!!.includeMain.bottomNavigation.menu.getItem(position)
binding!!.includeMain.toolbar.title = item.title
binding!!.includeMain.toolbar.menu.clear()
when (item.title) {
getString(R.string.menu_rules) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_rules
)
getString(R.string.menu_senders) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_senders
)
getString(R.string.menu_settings) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_settings
)
else -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
}
item.isChecked = true
updateSideNavStatus(item)
}
override fun onPageScrollStateChanged(state: Int) {}
})
binding!!.includeMain.bottomNavigation.setOnNavigationItemSelectedListener(this)
//tabBar分类切换
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).observe(this) { type: String ->
logsType = type
}
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).observe(this) { type: String ->
ruleType = type
}
//更新通知栏文案
LiveEventBus.get(EVENT_UPDATE_NOTIFY, String::class.java).observe(this) { notify: String ->
cactusUpdateNotification {
setContent(notify)
}
}
}
/**
* 处理侧边栏点击事件
*
* @param menuItem
* @return
*/
private fun handleNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
return true
}
}
return false
}
@SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_notifications -> {
showTipsForce(this)
}
R.id.action_clear_logs -> {
MaterialDialog.Builder(this)
.content(R.string.delete_type_log_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
AppDatabase.getInstance(this)
.msgDao()
.deleteAll(logsType)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : CompletableObserver {
override fun onSubscribe(d: Disposable) {}
override fun onComplete() {
XToastUtils.success(R.string.delete_type_log_toast)
}
override fun onError(e: Throwable) {
e.message?.let { XToastUtils.error(it) }
}
})
}
.show()
}
R.id.action_add_sender -> {
val dialog = BottomSheetDialog(this)
val view: View =
LayoutInflater.from(this).inflate(R.layout.dialog_sender_bottom_sheet, null)
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f))
val widgetItemAdapter = WidgetItemAdapter(SENDER_FRAGMENT_LIST)
widgetItemAdapter.setOnItemClickListener(this)
recyclerView.adapter = widgetItemAdapter
dialog.setContentView(view)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
dialog.show()
WidgetUtils.transparentBottomSheetDialogBackground(dialog)
}
R.id.action_add_rule -> {
PageOption.to(RulesEditFragment::class.java)
.putString(KEY_RULE_TYPE, ruleType)
.setNewActivity(true)
.open(this)
}
/*R.id.action_restore_settings -> {
XToastUtils.success(logsType)
}*/
}
return false
}
@SingleClick
override fun onClick(v: View) {
}
//================Navigation================//
/**
* 底部导航栏点击事件
*
* @param menuItem
* @return
*/
override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
updateSideNavStatus(menuItem)
return true
}
}
return false
}
/**
* 更新侧边栏菜单选中状态
*
* @param menuItem
*/
private fun updateSideNavStatus(menuItem: MenuItem) {
val side = binding!!.navView.menu.findItem(menuItem.itemId)
if (side != null) {
side.isChecked = true
}
}
//按返回键不退出回到桌面
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_HOME)
startActivity(intent)
}
@SingleClick
override fun onItemClick(itemView: View, widgetInfo: PageInfo, pos: Int) {
try {
@Suppress("UNCHECKED_CAST")
PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setNewActivity(true)
.putInt(KEY_SENDER_TYPE, pos) //注意:目前刚好是这个顺序而已
.open(this)
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(e.message.toString())
}
}
//动态加载FrpcLib
private fun downloadFrpcLib() {
val cpuAbi = when (Build.CPU_ABI) {
"x86" -> "x86"
"x86_64" -> "x86_64"
"arm64-v8a" -> "arm64-v8a"
else -> "armeabi-v7a"
}
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)
if (!soFile.exists()) soFile.mkdirs()
val downloadUrl = String.format(FRPC_LIB_DOWNLOAD_URL, FRPC_LIB_VERSION, cpuAbi)
val mContext = this
val dialog: MaterialDialog = MaterialDialog.Builder(mContext)
.title(String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION))
.content(getString(R.string.frpclib_download_content))
.contentGravity(GravityEnum.CENTER)
.progress(false, 0, true)
.progressNumberFormat("%2dMB/%1dMB")
.build()
XHttp.downLoad(downloadUrl)
.savePath(cacheDir.absolutePath)
.execute(object : DownloadProgressCallBack<String?>() {
override fun onStart() {
dialog.show()
}
override fun onError(e: ApiException) {
dialog.dismiss()
XToastUtils.error(e.message.toString())
}
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
Log.d(TAG, "onProgress: bytesRead=$bytesRead, contentLength=$contentLength")
dialog.maxProgress = (contentLength / 1048576L).toInt()
dialog.setProgress((bytesRead / 1048576L).toInt())
}
override fun onComplete(srcPath: String) {
dialog.dismiss()
Log.d(TAG, "srcPath = $srcPath")
val srcFile = File(srcPath)
val destFile = File("$libPath/libgojni.so")
FileUtils.moveFile(srcFile, destFile, null)
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid()) //杀掉以前进程
}
})
}
}

@ -1,57 +1,56 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.adapter.LogsPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
class LogsPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<LogsAndRuleAndSender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterLogsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.tvFrom.text = item.logs.from
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.logs.time)
holder.binding.tvContent.text = item.logs.content
holder.binding.ivSenderImage.setImageResource(Sender.getImageId(item.relation.sender.type))
holder.binding.ivStatusImage.setImageResource(item.logs.statusImageId)
holder.binding.ivSimImage.setImageResource(item.logs.simImageId)
holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterLogsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: LogsAndRuleAndSender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<LogsAndRuleAndSender> = object : DiffUtil.ItemCallback<LogsAndRuleAndSender>() {
override fun areItemsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs.id == newItem.logs.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs === newItem.logs
}
}
}
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.adapter.LogsPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
class LogsPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<LogsAndRuleAndSender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterLogsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.tvFrom.text = item.msg.from
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.logs.time)
holder.binding.tvContent.text = item.msg.content
//holder.binding.ivSenderImage.setImageResource(Sender.getImageId(item.sender.type))
//holder.binding.ivStatusImage.setImageResource(item.logs.statusImageId)
holder.binding.ivSimImage.setImageResource(item.msg.simImageId)
holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterLogsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: LogsAndRuleAndSender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<LogsAndRuleAndSender> = object : DiffUtil.ItemCallback<LogsAndRuleAndSender>() {
override fun areItemsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs.id == newItem.logs.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs === newItem.logs
}
}
}
}

@ -0,0 +1,78 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.MsgPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
class MsgPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<MsgAndLogs, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterLogsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.tvFrom.text = item.msg.from
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.msg.time)
holder.binding.tvContent.text = item.msg.content
//holder.binding.ivSenderImage.setImageResource(Sender.getImageId(item.sender.type))
//holder.binding.ivStatusImage.setImageResource(item.msg.statusImageId)
holder.binding.ivSimImage.setImageResource(item.msg.simImageId)
holder.binding.layoutLogs.removeAllViews()
for (logs in item.logsList) {
val layoutSenderItem = View.inflate(App.context, R.layout.item_logs, null) as LinearLayout
val ivSenderImage = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_image)
val ivSenderStatus = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_status)
val tvSenderName = layoutSenderItem.findViewById<TextView>(R.id.tv_sender_name)
ivSenderImage.setImageResource(logs.senderImageId)
ivSenderStatus.setImageResource(logs.statusImageId)
tvSenderName.text = logs.senderName
layoutSenderItem.setOnClickListener { view: View? ->
itemClickListener.onLogsClicked(view, logs)
}
holder.binding.layoutLogs.addView(layoutSenderItem)
}
holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterLogsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: MsgAndLogs)
fun onLogsClicked(view: View?, item: LogsDetail)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<MsgAndLogs> = object : DiffUtil.ItemCallback<MsgAndLogs>() {
override fun areItemsTheSame(oldItem: MsgAndLogs, newItem: MsgAndLogs): Boolean {
return oldItem.msg.id == newItem.msg.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: MsgAndLogs, newItem: MsgAndLogs): Boolean {
return oldItem.msg === newItem.msg
}
}
}
}

@ -1,68 +1,78 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.RulePagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import com.idormy.sms.forwarder.databinding.AdapterRulesCardViewListItemBinding
class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<RuleAndSender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterRulesCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivRuleImage.setImageResource(item.rule.imageId)
holder.binding.ivRuleStatus.setImageResource(item.rule.statusImageId)
holder.binding.tvRuleMatch.text = item.rule.ruleMatch
holder.binding.ivSenderImage.setImageResource(item.sender.imageId)
holder.binding.ivSenderStatus.setImageResource(item.sender.statusImageId)
holder.binding.tvSenderName.text = item.sender.name
/*holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}*/
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterRulesCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: RuleAndSender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<RuleAndSender> = object : DiffUtil.ItemCallback<RuleAndSender>() {
override fun areItemsTheSame(oldItem: RuleAndSender, newItem: RuleAndSender): Boolean {
return oldItem.rule.id == newItem.rule.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: RuleAndSender, newItem: RuleAndSender): Boolean {
return oldItem.rule === newItem.rule
}
}
}
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.RulePagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.databinding.AdapterRulesCardViewListItemBinding
class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Rule, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterRulesCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivRuleImage.setImageResource(item.imageId)
holder.binding.ivRuleStatus.setImageResource(item.statusImageId)
holder.binding.tvRuleMatch.text = item.ruleMatch
holder.binding.layoutSenders.removeAllViews()
for (sender in item.senderList) {
val layoutSenderItem = View.inflate(App.context, R.layout.item_sender, null) as LinearLayout
val ivSenderImage = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_image)
val ivSenderStatus = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_status)
val tvSenderName = layoutSenderItem.findViewById<TextView>(R.id.tv_sender_name)
ivSenderImage.setImageResource(sender.imageId)
ivSenderStatus.setImageResource(sender.statusImageId)
tvSenderName.text = sender.name
holder.binding.layoutSenders.addView(layoutSenderItem)
}
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterRulesCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: Rule)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<Rule> = object : DiffUtil.ItemCallback<Rule>() {
override fun areItemsTheSame(oldItem: Rule, newItem: Rule): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Rule, newItem: Rule): Boolean {
return oldItem === newItem
}
}
}
}

@ -1,60 +1,58 @@
package com.idormy.sms.forwarder.core
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.Configuration
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.service.ForegroundService
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
lateinit var app: Application
val frpc: FrpcRepository by lazy { (app as App).frpcRepository }
val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository }
/*
val telephonyManager: TelephonyManager by lazy { app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager }
val smsManager: SmsManager by lazy { app.getSystemService(SmsManager::class.java) }
val subscriptionManager: SubscriptionManager by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
SubscriptionManager.from(app)
} else {
app.getSystemService(SubscriptionManager::class.java)
}
}
val user by lazy { app.getSystemService<UserManager>()!! }*/
/*val directBootAware: Boolean get() = directBootSupported && dataStore.canToggleLocked
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && try {
app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
} catch (_: RuntimeException) {
false
}
}*/
fun init(app: Application) {
this.app = app
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().apply {
setDefaultProcessName(app.packageName + ":bg")
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
setExecutor { (app as App).applicationScope.launch { it.run() } }
setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
}.build()
}
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ForegroundService::class.java))
}
package com.idormy.sms.forwarder.core
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.Configuration
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.*
import com.idormy.sms.forwarder.service.ForegroundService
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
lateinit var app: Application
val frpc: FrpcRepository by lazy { (app as App).frpcRepository }
val msg: MsgRepository by lazy { (app as App).msgRepository }
val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository }
/*
val telephonyManager: TelephonyManager by lazy { app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager }
val smsManager: SmsManager by lazy { app.getSystemService(SmsManager::class.java) }
val subscriptionManager: SubscriptionManager by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
SubscriptionManager.from(app)
} else {
app.getSystemService(SubscriptionManager::class.java)
}
}
val user by lazy { app.getSystemService<UserManager>()!! }*/
/*val directBootAware: Boolean get() = directBootSupported && dataStore.canToggleLocked
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && try {
app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
} catch (_: RuntimeException) {
false
}
}*/
fun init(app: Application) {
this.app = app
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().apply {
setDefaultProcessName(app.packageName + ":bg")
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
setExecutor { (app as App).applicationScope.launch { it.run() } }
setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
}.build()
}
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ForegroundService::class.java))
}

@ -7,26 +7,22 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.idormy.sms.forwarder.database.dao.FrpcDao
import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.ext.Converters
import com.idormy.sms.forwarder.database.dao.*
import com.idormy.sms.forwarder.database.entity.*
import com.idormy.sms.forwarder.database.ext.ConvertersDate
import com.idormy.sms.forwarder.utils.DATABASE_NAME
@Database(
entities = [Frpc::class, Logs::class, Rule::class, Sender::class],
version = 11,
entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class],
views = [LogsDetail::class],
version = 15,
exportSchema = false
)
@TypeConverters(Converters::class)
@TypeConverters(ConvertersDate::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun frpcDao(): FrpcDao
abstract fun msgDao(): MsgDao
abstract fun logsDao(): LogsDao
abstract fun ruleDao(): RuleDao
abstract fun senderDao(): SenderDao
@ -96,6 +92,10 @@ custom_domains = smsf.demo.com
MIGRATION_8_9,
MIGRATION_9_10,
MIGRATION_10_11,
MIGRATION_11_12,
MIGRATION_12_13,
MIGRATION_13_14,
MIGRATION_14_15,
)
/*if (BuildConfig.DEBUG) {
@ -281,9 +281,89 @@ CREATE TABLE "Sender" (
//转发日志添加SIM卡槽ID
private val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Logs add column sub_id INTEGER NOT NULL DEFAULT 0 ")
database.execSQL("Alter table Logs add column sub_id INTEGER NOT NULL DEFAULT 0")
}
}
//单个转发规则可绑定多个发送通道
private val MIGRATION_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Logs add column sender_id INTEGER NOT NULL DEFAULT 0")
database.execSQL("Update Logs Set sender_id = (Select sender_id from Rule where Logs.rule_id = Rule.id)")
database.execSQL("Alter table Rule add column sender_list TEXT NOT NULL DEFAULT ''")
database.execSQL("Update Rule set sender_list = sender_id")
database.execSQL("CREATE INDEX \"index_Rule_sender_ids\" ON \"Rule\" ( \"sender_list\" ASC)")
//删除字段sender_id
/*database.execSQL("Create table Rule_t as Select id,type,filed,check,value,sender_list,sms_template,regex_replace,sim_slot,status,time from Rule where 1 = 1")
database.execSQL("Drop table Rule")
database.execSQL("Alter table Rule_t rename to Rule")
database.execSQL("CREATE UNIQUE INDEX \"index_Rule_id\" ON \"Rule\" ( \"id\" ASC)")*/
}
}
//转发规则添加发送通道逻辑
private val MIGRATION_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Rule add column sender_logic TEXT NOT NULL DEFAULT 'ALL'")
}
}
//分割Logs表
private val MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
//database.execSQL("Create table Msg as Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
database.execSQL(
"""
CREATE TABLE "Msg" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"from" TEXT NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"sim_slot" INTEGER NOT NULL DEFAULT -1,
"sim_info" TEXT NOT NULL DEFAULT '',
"sub_id" INTEGER NOT NULL DEFAULT 0,
"time" INTEGER NOT NULL
)
""".trimIndent()
)
database.execSQL("INSERT INTO Msg (id,type,`from`,content,sim_slot,sim_info,sub_id,time) Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
database.execSQL("CREATE UNIQUE INDEX \"index_Msg_id\" ON \"Msg\" ( \"id\" ASC)")
database.execSQL("ALTER TABLE Logs RENAME TO Logs_old")
//database.execSQL("Create table Logs_new as Select id,id as msg_id,rule_id,sender_id,forward_status,forward_response,time from Logs where 1 = 1")
database.execSQL(
"""
CREATE TABLE "Logs" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"msg_id" INTEGER NOT NULL DEFAULT 0,
"rule_id" INTEGER NOT NULL DEFAULT 0,
"sender_id" INTEGER NOT NULL DEFAULT 0,
"forward_status" INTEGER NOT NULL DEFAULT 1,
"forward_response" TEXT NOT NULL DEFAULT '',
"time" INTEGER NOT NULL,
FOREIGN KEY ("msg_id") REFERENCES "Msg" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("rule_id") REFERENCES "Rule" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("sender_id") REFERENCES "Sender" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
""".trimIndent()
)
database.execSQL("INSERT INTO Logs (id,type,msg_id,rule_id,sender_id,forward_status,forward_response,time) SELECT id,type,id as msg_id,rule_id,sender_id,forward_status,forward_response,time FROM Logs_old")
database.execSQL("DROP TABLE Logs_old")
database.execSQL("CREATE UNIQUE INDEX \"index_Logs_id\" ON \"Logs\" ( \"id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_msg_id\" ON \"Logs\" ( \"msg_id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_rule_id\" ON \"Logs\" ( \"rule_id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_sender_id\" ON \"Logs\" ( \"sender_id\" ASC)")
}
}
// 定义数据库迁移配置
private val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
// 这里新建一个视图(视图名称要用两个半角的间隔号括起来)
database.execSQL("CREATE VIEW `LogsDetail` AS SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")
}
}
}
}

@ -0,0 +1,41 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface MsgDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(msg: Msg): Long
@Delete
fun delete(msg: Msg): Completable
@Query("DELETE FROM Msg where id=:id")
fun delete(id: Long)
@Query("DELETE FROM Msg where type=:type")
fun deleteAll(type: String): Completable
@Query("DELETE FROM Msg where time<:time")
fun deleteTimeAgo(time: Long)
@Update
fun update(msg: Msg): Completable
@Query("SELECT * FROM Msg where id=:id")
fun get(id: Long): Single<Msg>
@Query("SELECT count(*) FROM Msg where type=:type")
fun count(type: String): Single<Int>
@Transaction
@Query("SELECT * FROM Msg WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, MsgAndLogs>
}

@ -25,6 +25,9 @@ interface RuleDao {
@Query("SELECT * FROM Rule where id=:id")
fun get(id: Long): Single<Rule>
@Query("SELECT * FROM Rule where id=:id")
fun getOne(id: Long): Rule
@Query("SELECT count(*) FROM Rule where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>
@ -40,7 +43,7 @@ interface RuleDao {
@Transaction
@Query("SELECT * FROM Rule where type=:type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, RuleAndSender>
fun pagingSource(type: String): PagingSource<Int, Rule>
@Transaction
@Query("SELECT * FROM Rule where type=:type and status=:status and (sim_slot='ALL' or sim_slot=:simSlot)")

@ -25,6 +25,9 @@ interface SenderDao {
@Query("SELECT * FROM Sender where id=:id")
fun get(id: Long): Single<Sender>
@Query("SELECT * FROM Sender where id=:id")
fun getOne(id: Long): Sender
@Query("SELECT count(*) FROM Sender where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>

@ -10,45 +10,47 @@ import java.util.*
@Entity(
tableName = "Logs",
foreignKeys = [
ForeignKey(
entity = Msg::class,
parentColumns = ["id"],
childColumns = ["msg_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
),
ForeignKey(
entity = Rule::class,
parentColumns = ["id"],
childColumns = ["rule_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
)
),
ForeignKey(
entity = Sender::class,
parentColumns = ["id"],
childColumns = ["sender_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
),
],
indices = [
Index(value = ["id"], unique = true),
Index(value = ["rule_id"])
Index(value = ["msg_id"]),
Index(value = ["rule_id"]),
Index(value = ["sender_id"]),
]
)
data class Logs(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "from", defaultValue = "") var from: String,
@ColumnInfo(name = "content", defaultValue = "") var content: String,
@ColumnInfo(name = "msg_id", defaultValue = "0") var msgId: Long = 0,
@ColumnInfo(name = "rule_id", defaultValue = "0") var ruleId: Long = 0,
@ColumnInfo(name = "sim_info", defaultValue = "") var simInfo: String = "",
@ColumnInfo(name = "sub_id", defaultValue = "0") var subId: Int = 0,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "forward_status", defaultValue = "1") var forwardStatus: Int = 1,
@ColumnInfo(name = "forward_response", defaultValue = "") var forwardResponse: String = "",
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
val simImageId: Int
get() {
if (simInfo.isNotEmpty()) {
if (simInfo.replace("-", "").startsWith("SIM2")) {
return R.drawable.ic_sim2 //mipmap
} else if (simInfo.replace("-", "").startsWith("SIM1")) {
return R.drawable.ic_sim1
}
}
return R.drawable.ic_sim
}
val statusImageId: Int
get() {
if (forwardStatus == 1) {

@ -1,18 +1,32 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.parcelize.Parcelize
@Parcelize
data class LogsAndRuleAndSender(
@Embedded val logs: Logs,
@Relation(
entity = Rule::class,
parentColumn = "rule_id",
entityColumn = "id"
)
val relation: RuleAndSender,
) : Parcelable
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.parcelize.Parcelize
@Parcelize
data class LogsAndRuleAndSender(
@Embedded val logs: Logs,
@Relation(
entity = Msg::class,
parentColumn = "msg_id",
entityColumn = "id"
)
val msg: Msg,
@Relation(
entity = Rule::class,
parentColumn = "rule_id",
entityColumn = "id"
)
val rule: Rule,
@Relation(
entity = Sender::class,
parentColumn = "sender_id",
entityColumn = "id"
)
val sender: Sender,
) : Parcelable

@ -0,0 +1,59 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.DatabaseView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.*
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@DatabaseView("SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")
data class LogsDetail(
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "msg_id", defaultValue = "0") var msgId: Long = 0,
@ColumnInfo(name = "rule_id", defaultValue = "0") var ruleId: Long = 0,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "forward_status", defaultValue = "1") var forwardStatus: Int = 1,
@ColumnInfo(name = "forward_response", defaultValue = "") var forwardResponse: String = "",
@ColumnInfo(name = "time") var time: Date = Date(),
@ColumnInfo(name = "rule_filed", defaultValue = "") var ruleFiled: String,
@ColumnInfo(name = "rule_check", defaultValue = "") var ruleCheck: String,
@ColumnInfo(name = "rule_value", defaultValue = "") var ruleValue: String,
@ColumnInfo(name = "rule_sim_slot", defaultValue = "") var ruleSimSlot: String,
@ColumnInfo(name = "sender_type", defaultValue = "1") var senderType: Int = 1,
@ColumnInfo(name = "sender_name", defaultValue = "") var senderName: String,
) : Parcelable {
val statusImageId: Int
get() {
if (forwardStatus == 1) {
return R.drawable.ic_round_warning
} else if (forwardStatus == 2) {
return R.drawable.ic_round_check
}
return R.drawable.ic_round_cancel
}
val senderImageId: Int
get() = when (senderType) {
TYPE_DINGTALK_GROUP_ROBOT -> R.drawable.icon_dingtalk
TYPE_EMAIL -> R.drawable.icon_email
TYPE_BARK -> R.drawable.icon_bark
TYPE_WEBHOOK -> R.drawable.icon_webhook
TYPE_WEWORK_ROBOT -> R.drawable.icon_wework_robot
TYPE_WEWORK_AGENT -> R.drawable.icon_wework_agent
TYPE_SERVERCHAN -> R.drawable.icon_serverchan
TYPE_TELEGRAM -> R.drawable.icon_telegram
TYPE_FEISHU -> R.drawable.icon_feishu
TYPE_PUSHPLUS -> R.drawable.icon_pushplus
TYPE_GOTIFY -> R.drawable.icon_gotify
TYPE_SMS -> R.drawable.icon_sms
TYPE_DINGTALK_INNER_ROBOT -> R.drawable.icon_dingtalk_inner
TYPE_FEISHU_APP -> R.drawable.icon_feishu_app
TYPE_URL_SCHEME -> R.drawable.icon_url_scheme
else -> R.drawable.icon_sms
}
}

@ -0,0 +1,43 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.R
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@Entity(
tableName = "Msg",
indices = [
Index(value = ["id"], unique = true)
]
)
data class Msg(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "from", defaultValue = "") var from: String,
@ColumnInfo(name = "content", defaultValue = "") var content: String,
@ColumnInfo(name = "sim_slot", defaultValue = "-1") var simSlot: Int = -1, //卡槽id-1=获取失败、0=卡槽1、1=卡槽2
@ColumnInfo(name = "sim_info", defaultValue = "") var simInfo: String = "",
@ColumnInfo(name = "sub_id", defaultValue = "0") var subId: Int = 0,
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
val simImageId: Int
get() {
if (simInfo.isNotEmpty()) {
if (simInfo.replace("-", "").startsWith("SIM2")) {
return R.drawable.ic_sim2 //mipmap
} else if (simInfo.replace("-", "").startsWith("SIM1")) {
return R.drawable.ic_sim1
}
}
return R.drawable.ic_sim
}
}

@ -0,0 +1,17 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.parcelize.Parcelize
@Parcelize
data class MsgAndLogs(
@Embedded val msg: Msg,
@Relation(
parentColumn = "id",
entityColumn = "msg_id"
)
val logsList: List<LogsDetail>
) : Parcelable

@ -1,180 +1,193 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import android.util.Log
import androidx.room.*
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xui.utils.ResUtils.getString
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
@Parcelize
@Entity(
tableName = "Rule",
foreignKeys = [
ForeignKey(
entity = Sender::class,
parentColumns = ["id"],
childColumns = ["sender_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
)
],
indices = [
Index(value = ["id"], unique = true),
Index(value = ["sender_id"])
]
)
data class Rule(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "filed", defaultValue = "transpond_all") var filed: String,
@ColumnInfo(name = "check", defaultValue = "is") var check: String,
@ColumnInfo(name = "value", defaultValue = "") var value: String,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "sms_template", defaultValue = "") var smsTemplate: String = "",
@ColumnInfo(name = "regex_replace", defaultValue = "") var regexReplace: String = "",
@ColumnInfo(name = "sim_slot", defaultValue = "ALL") var simSlot: String = "",
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1,
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
companion object {
val TAG: String = Rule::class.java.simpleName
fun getRuleMatch(filed: String?, check: String?, value: String?, simSlot: String?): Any {
val sb = StringBuilder()
sb.append(SIM_SLOT_MAP[simSlot]).append(getString(R.string.rule_card))
if (filed == null || filed == FILED_TRANSPOND_ALL) {
sb.append(getString(R.string.rule_all_fw_to))
} else {
sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(value).append(getString(R.string.rule_fw_to))
}
return sb.toString()
}
}
val ruleMatch: String
get() {
val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
return if (filed == FILED_TRANSPOND_ALL) {
simStr + getString(R.string.rule_all_fw_to)
} else {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
}
}
val statusChecked: Boolean
get() = status != STATUS_OFF
val imageId: Int
get() = when (simSlot) {
CHECK_SIM_SLOT_1 -> R.drawable.ic_sim1
CHECK_SIM_SLOT_2 -> R.drawable.ic_sim2
CHECK_SIM_SLOT_ALL -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
else -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
}
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
}
fun getSimSlotCheckId(): Int {
return when (simSlot) {
CHECK_SIM_SLOT_1 -> R.id.rb_sim_slot_1
CHECK_SIM_SLOT_2 -> R.id.rb_sim_slot_2
else -> R.id.rb_sim_slot_all
}
}
fun getFiledCheckId(): Int {
return when (filed) {
FILED_MSG_CONTENT -> R.id.rb_content
FILED_PHONE_NUM -> R.id.rb_phone
FILED_PACKAGE_NAME -> R.id.rb_package_name
FILED_INFORM_CONTENT -> R.id.rb_inform_content
FILED_MULTI_MATCH -> R.id.rb_multi_match
else -> R.id.rb_transpond_all
}
}
fun getCheckCheckId(): Int {
return when (check) {
CHECK_CONTAIN -> R.id.rb_contain
CHECK_NOT_CONTAIN -> R.id.rb_not_contain
CHECK_START_WITH -> R.id.rb_start_with
CHECK_END_WITH -> R.id.rb_end_with
CHECK_REGEX -> R.id.rb_regex
else -> R.id.rb_is
}
}
//字段分支
@Throws(Exception::class)
fun checkMsg(msg: MsgInfo?): Boolean {
//检查这一行和上一行合并的结果是否命中
var mixChecked = false
if (msg != null) {
//先检查规则是否命中
when (this.filed) {
FILED_TRANSPOND_ALL -> mixChecked = true
FILED_PHONE_NUM, FILED_PACKAGE_NAME -> mixChecked = checkValue(msg.from)
FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
else -> {}
}
}
Log.i(TAG, "rule:$this checkMsg:$msg checked:$mixChecked")
return mixChecked
}
//内容分支
private fun checkValue(msgValue: String?): Boolean {
var checked = false
when (this.check) {
CHECK_IS -> checked = this.value == msgValue
CHECK_NOT_IS -> checked = this.value != msgValue
CHECK_CONTAIN -> if (msgValue != null) {
checked = msgValue.contains(this.value)
}
CHECK_NOT_CONTAIN -> if (msgValue != null) {
checked = !msgValue.contains(this.value)
}
CHECK_START_WITH -> if (msgValue != null) {
checked = msgValue.startsWith(this.value)
}
CHECK_END_WITH -> if (msgValue != null) {
checked = msgValue.endsWith(this.value)
}
CHECK_REGEX -> if (msgValue != null) {
try {
//checked = Pattern.matches(this.value, msgValue);
val pattern = Pattern.compile(this.value, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(msgValue)
while (matcher.find()) {
checked = true
break
}
} catch (e: PatternSyntaxException) {
Log.d(TAG, "PatternSyntaxException: ")
Log.d(TAG, "Description: " + e.description)
Log.d(TAG, "Index: " + e.index)
Log.d(TAG, "Message: " + e.message)
Log.d(TAG, "Pattern: " + e.pattern)
}
}
else -> {}
}
Log.i(TAG, "checkValue " + msgValue + " " + this.check + " " + this.value + " checked:" + checked)
return checked
}
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import android.util.Log
import androidx.room.*
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.database.ext.ConvertersSenderList
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xui.utils.ResUtils.getString
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
@Parcelize
@Entity(
tableName = "Rule",
foreignKeys = [
ForeignKey(
entity = Sender::class,
parentColumns = ["id"],
childColumns = ["sender_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
)
],
indices = [
Index(value = ["id"], unique = true),
Index(value = ["sender_id"]),
Index(value = ["sender_list"])
]
)
@TypeConverters(ConvertersSenderList::class)
data class Rule(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "filed", defaultValue = "transpond_all") var filed: String,
@ColumnInfo(name = "check", defaultValue = "is") var check: String,
@ColumnInfo(name = "value", defaultValue = "") var value: String,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "sms_template", defaultValue = "") var smsTemplate: String = "",
@ColumnInfo(name = "regex_replace", defaultValue = "") var regexReplace: String = "",
@ColumnInfo(name = "sim_slot", defaultValue = "ALL") var simSlot: String = "",
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1,
@ColumnInfo(name = "time") var time: Date = Date(),
@ColumnInfo(name = "sender_list", defaultValue = "") var senderList: List<Sender>,
@ColumnInfo(name = "sender_logic", defaultValue = "ALL") var senderLogic: String = "ALL",
) : Parcelable {
companion object {
val TAG: String = Rule::class.java.simpleName
fun getRuleMatch(filed: String?, check: String?, value: String?, simSlot: String?): Any {
val sb = StringBuilder()
sb.append(SIM_SLOT_MAP[simSlot]).append(getString(R.string.rule_card))
if (filed == null || filed == FILED_TRANSPOND_ALL) {
sb.append(getString(R.string.rule_all_fw_to))
} else {
sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(value).append(getString(R.string.rule_fw_to))
}
return sb.toString()
}
}
val ruleMatch: String
get() {
val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
return if (filed == FILED_TRANSPOND_ALL) {
simStr + getString(R.string.rule_all_fw_to)
} else {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
}
}
val statusChecked: Boolean
get() = status != STATUS_OFF
val imageId: Int
get() = when (simSlot) {
CHECK_SIM_SLOT_1 -> R.drawable.ic_sim1
CHECK_SIM_SLOT_2 -> R.drawable.ic_sim2
CHECK_SIM_SLOT_ALL -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
else -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
}
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
}
fun getSenderLogicCheckId(): Int {
return when (senderLogic) {
SENDER_LOGIC_UNTIL_FAIL -> R.id.rb_sender_logic_until_fail
SENDER_LOGIC_UNTIL_SUCCESS -> R.id.rb_sender_logic_until_success
else -> R.id.rb_sender_logic_all
}
}
fun getSimSlotCheckId(): Int {
return when (simSlot) {
CHECK_SIM_SLOT_1 -> R.id.rb_sim_slot_1
CHECK_SIM_SLOT_2 -> R.id.rb_sim_slot_2
else -> R.id.rb_sim_slot_all
}
}
fun getFiledCheckId(): Int {
return when (filed) {
FILED_MSG_CONTENT -> R.id.rb_content
FILED_PHONE_NUM -> R.id.rb_phone
FILED_PACKAGE_NAME -> R.id.rb_package_name
FILED_INFORM_CONTENT -> R.id.rb_inform_content
FILED_MULTI_MATCH -> R.id.rb_multi_match
else -> R.id.rb_transpond_all
}
}
fun getCheckCheckId(): Int {
return when (check) {
CHECK_CONTAIN -> R.id.rb_contain
CHECK_NOT_CONTAIN -> R.id.rb_not_contain
CHECK_START_WITH -> R.id.rb_start_with
CHECK_END_WITH -> R.id.rb_end_with
CHECK_REGEX -> R.id.rb_regex
else -> R.id.rb_is
}
}
//字段分支
@Throws(Exception::class)
fun checkMsg(msg: MsgInfo?): Boolean {
//检查这一行和上一行合并的结果是否命中
var mixChecked = false
if (msg != null) {
//先检查规则是否命中
when (this.filed) {
FILED_TRANSPOND_ALL -> mixChecked = true
FILED_PHONE_NUM, FILED_PACKAGE_NAME -> mixChecked = checkValue(msg.from)
FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
else -> {}
}
}
Log.i(TAG, "rule:$this checkMsg:$msg checked:$mixChecked")
return mixChecked
}
//内容分支
private fun checkValue(msgValue: String?): Boolean {
var checked = false
when (this.check) {
CHECK_IS -> checked = this.value == msgValue
CHECK_NOT_IS -> checked = this.value != msgValue
CHECK_CONTAIN -> if (msgValue != null) {
checked = msgValue.contains(this.value)
}
CHECK_NOT_CONTAIN -> if (msgValue != null) {
checked = !msgValue.contains(this.value)
}
CHECK_START_WITH -> if (msgValue != null) {
checked = msgValue.startsWith(this.value)
}
CHECK_END_WITH -> if (msgValue != null) {
checked = msgValue.endsWith(this.value)
}
CHECK_REGEX -> if (msgValue != null) {
try {
//checked = Pattern.matches(this.value, msgValue);
val pattern = Pattern.compile(this.value, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(msgValue)
while (matcher.find()) {
checked = true
break
}
} catch (e: PatternSyntaxException) {
Log.d(TAG, "PatternSyntaxException: ")
Log.d(TAG, "Description: " + e.description)
Log.d(TAG, "Index: " + e.index)
Log.d(TAG, "Message: " + e.message)
Log.d(TAG, "Pattern: " + e.pattern)
}
}
else -> {}
}
Log.i(TAG, "checkValue " + msgValue + " " + this.check + " " + this.value + " checked:" + checked)
return checked
}
}

@ -1,17 +1,17 @@
package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import java.util.*
@Suppress("unused")
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import java.util.*
@Suppress("unused")
class ConvertersDate {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}

@ -0,0 +1,29 @@
package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Sender
import java.util.*
@Suppress("unused")
class ConvertersSenderList {
@TypeConverter
fun stringToObject(value: String): List<Sender> {
var senderList: MutableList<Sender> = mutableListOf()
value.split(",").map { it.trim() }.forEach {
val sender = Core.sender.getOne(it.toLong())
senderList.add(sender)
}
return senderList
}
@TypeConverter
fun objectToString(list: List<Sender>): String {
var senderList = ArrayList<Long>()
list.forEach {
senderList += it.id
}
return senderList.joinToString(",")
}
}

@ -0,0 +1,22 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.entity.Msg
class MsgRepository(private val msgDao: MsgDao) {
@WorkerThread
fun delete(id: Long) {
msgDao.delete(id)
}
@WorkerThread
fun deleteTimeAgo(time: Long) {
msgDao.deleteTimeAgo(time)
}
@WorkerThread
suspend fun insert(msg: Msg): Long = msgDao.insert(msg)
}

@ -1,36 +1,39 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule
class RuleRepository(
private val ruleDao: RuleDao,
) {
var listener: Listener? = null
@WorkerThread
fun insert(rule: Rule) {
ruleDao.insert(rule)
}
@WorkerThread
fun delete(id: Long) {
listener?.onDelete(id)
ruleDao.delete(id)
}
@WorkerThread
fun get(id: Long) = ruleDao.get(id)
suspend fun getRuleAndSender(type: String, status: Int, simSlot: String) = ruleDao.getRuleAndSender(type, status, simSlot)
fun getRuleList(type: String, status: Int, simSlot: String) = ruleDao.getRuleList(type, status, simSlot)
@WorkerThread
fun update(rule: Rule) = ruleDao.update(rule)
//TODO:允许主线程访问,后面再优化
val all: List<Rule> = ruleDao.getAll()
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule
class RuleRepository(
private val ruleDao: RuleDao,
) {
var listener: Listener? = null
@WorkerThread
fun insert(rule: Rule) {
ruleDao.insert(rule)
}
@WorkerThread
fun delete(id: Long) {
listener?.onDelete(id)
ruleDao.delete(id)
}
@WorkerThread
fun get(id: Long) = ruleDao.get(id)
@WorkerThread
fun getOne(id: Long) = ruleDao.getOne(id)
suspend fun getRuleAndSender(type: String, status: Int, simSlot: String) = ruleDao.getRuleAndSender(type, status, simSlot)
fun getRuleList(type: String, status: Int, simSlot: String) = ruleDao.getRuleList(type, status, simSlot)
@WorkerThread
fun update(rule: Rule) = ruleDao.update(rule)
//TODO:允许主线程访问,后面再优化
val all: List<Rule> = ruleDao.getAll()
}

@ -1,34 +1,36 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.entity.Sender
import kotlinx.coroutines.flow.Flow
class SenderRepository(private val senderDao: SenderDao) {
var listener: Listener? = null
@WorkerThread
fun insert(sender: Sender) = senderDao.insert(sender)
@WorkerThread
fun delete(id: Long) {
listener?.onDelete(id)
senderDao.delete(id)
}
fun get(id: Long) = senderDao.get(id)
fun update(sender: Sender) = senderDao.update(sender)
val count: Flow<Long> = senderDao.getOnCount()
//TODO:允许主线程访问,后面再优化
val all: List<Sender> = senderDao.getAll2()
fun deleteAll() {
senderDao.deleteAll()
}
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.entity.Sender
import kotlinx.coroutines.flow.Flow
class SenderRepository(private val senderDao: SenderDao) {
var listener: Listener? = null
@WorkerThread
fun insert(sender: Sender) = senderDao.insert(sender)
@WorkerThread
fun delete(id: Long) {
listener?.onDelete(id)
senderDao.delete(id)
}
fun get(id: Long) = senderDao.get(id)
fun getOne(id: Long) = senderDao.getOne(id)
fun update(sender: Sender) = senderDao.update(sender)
val count: Flow<Long> = senderDao.getOnCount()
//TODO:允许主线程访问,后面再优化
val all: List<Sender> = senderDao.getAll2()
fun deleteAll() {
senderDao.deleteAll()
}
}

@ -1,40 +1,45 @@
package com.idormy.sms.forwarder.database.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.idormy.sms.forwarder.database.AppDatabase
class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (context == null) throw IllegalArgumentException("Context CAN NOT BE null")
when {
modelClass.isAssignableFrom(FrpcViewModel::class.java) -> {
val frpcDao = AppDatabase.getInstance(context).frpcDao()
@Suppress("UNCHECKED_CAST")
return FrpcViewModel(frpcDao) as T
}
modelClass.isAssignableFrom(LogsViewModel::class.java) -> {
val logDao = AppDatabase.getInstance(context).logsDao()
@Suppress("UNCHECKED_CAST")
return LogsViewModel(logDao) as T
}
modelClass.isAssignableFrom(RuleViewModel::class.java) -> {
val ruleDao = AppDatabase.getInstance(context).ruleDao()
@Suppress("UNCHECKED_CAST")
return RuleViewModel(ruleDao) as T
}
modelClass.isAssignableFrom(SenderViewModel::class.java) -> {
val senderDao = AppDatabase.getInstance(context).senderDao()
@Suppress("UNCHECKED_CAST")
return SenderViewModel(senderDao) as T
}
}
throw IllegalArgumentException("Unknown ViewModel class")
}
package com.idormy.sms.forwarder.database.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.idormy.sms.forwarder.database.AppDatabase
class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (context == null) throw IllegalArgumentException("Context CAN NOT BE null")
when {
modelClass.isAssignableFrom(FrpcViewModel::class.java) -> {
val frpcDao = AppDatabase.getInstance(context).frpcDao()
@Suppress("UNCHECKED_CAST")
return FrpcViewModel(frpcDao) as T
}
modelClass.isAssignableFrom(MsgViewModel::class.java) -> {
val msgDao = AppDatabase.getInstance(context).msgDao()
@Suppress("UNCHECKED_CAST")
return MsgViewModel(msgDao) as T
}
modelClass.isAssignableFrom(LogsViewModel::class.java) -> {
val logDao = AppDatabase.getInstance(context).logsDao()
@Suppress("UNCHECKED_CAST")
return LogsViewModel(logDao) as T
}
modelClass.isAssignableFrom(RuleViewModel::class.java) -> {
val ruleDao = AppDatabase.getInstance(context).ruleDao()
@Suppress("UNCHECKED_CAST")
return RuleViewModel(ruleDao) as T
}
modelClass.isAssignableFrom(SenderViewModel::class.java) -> {
val senderDao = AppDatabase.getInstance(context).senderDao()
@Suppress("UNCHECKED_CAST")
return SenderViewModel(senderDao) as T
}
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

@ -0,0 +1,36 @@
package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class MsgViewModel(private val dao: MsgDao) : ViewModel() {
private var type: String = "sms"
fun setType(type: String): MsgViewModel {
this.type = type
return this
}
val allMsg: Flow<PagingData<MsgAndLogs>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
initialLoadSize = 10
)
) {
dao.pagingSource(type)
}.flow.cachedIn(viewModelScope)
fun delete(id: Long) = ioThread {
dao.delete(id)
}
}

@ -1,38 +1,37 @@
package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class RuleViewModel(private val dao: RuleDao) : ViewModel() {
private var type: String = "sms"
fun setType(type: String): RuleViewModel {
this.type = type
return this
}
val allRules: Flow<PagingData<RuleAndSender>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
initialLoadSize = 10
)
) { dao.pagingSource(type) }.flow.cachedIn(viewModelScope)
fun insertOrUpdate(rule: Rule) = ioThread {
if (rule.id > 0) dao.update(rule) else dao.insert(rule)
}
fun delete(id: Long) = ioThread {
dao.delete(id)
}
package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class RuleViewModel(private val dao: RuleDao) : ViewModel() {
private var type: String = "sms"
fun setType(type: String): RuleViewModel {
this.type = type
return this
}
val allRules: Flow<PagingData<Rule>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
initialLoadSize = 10
)
) { dao.pagingSource(type) }.flow.cachedIn(viewModelScope)
fun insertOrUpdate(rule: Rule) = ioThread {
if (rule.id > 0) dao.update(rule) else dao.insert(rule)
}
fun delete(id: Long) = ioThread {
dao.delete(id)
}
}

@ -1,139 +1,160 @@
package com.idormy.sms.forwarder.fragment
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.LogsPagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.LogsViewModel
import com.idormy.sms.forwarder.databinding.FragmentLogsBinding
import com.idormy.sms.forwarder.utils.EVENT_UPDATE_LOGS_TYPE
import com.idormy.sms.forwarder.utils.FORWARD_STATUS_MAP
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.utils.ResUtils
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.xutil.data.DateUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@Suppress("PropertyName")
@Page(name = "转发日志")
class LogsFragment : BaseFragment<FragmentLogsBinding?>(), LogsPagingAdapter.OnItemClickListener {
val TAG: String = LogsFragment::class.java.simpleName
private var adapter = LogsPagingAdapter(this)
private val viewModel by viewModels<LogsViewModel> { BaseViewModelFactory(context) }
private var currentType: String = "sms"
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentLogsBinding {
return FragmentLogsBinding.inflate(inflater, container, false)
}
/**
* @return 返回为 null意为不需要导航栏
*/
override fun initTitle(): TitleBar? {
return null
}
/**
* 初始化控件
*/
override fun initViews() {
val virtualLayoutManager = VirtualLayoutManager(requireContext())
binding!!.recyclerView.layoutManager = virtualLayoutManager
val viewPool = RecycledViewPool()
binding!!.recyclerView.setRecycledViewPool(viewPool)
viewPool.setMaxRecycledViews(0, 10)
binding!!.recyclerView.isFocusableInTouchMode = false
binding!!.tabBar.setTabTitles(ResUtils.getStringArray(R.array.type_param_option))
binding!!.tabBar.setOnTabClickListener { _, position ->
//XToastUtils.toast("点击了$title--$position")
currentType = when (position) {
1 -> "call"
2 -> "app"
else -> "sms"
}
viewModel.setType(currentType)
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).post(currentType)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
}
}
override fun initListeners() {
binding!!.recyclerView.adapter = adapter
//下拉刷新
binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout ->
//adapter.refresh()
lifecycleScope.launch {
viewModel.setType(currentType).allLogs.collectLatest { adapter.submitData(it) }
}
refreshLayout.finishRefresh()
}
binding!!.refreshLayout.autoRefresh()
}
override fun onItemClicked(view: View?, item: LogsAndRuleAndSender) {
val ruleStr = StringBuilder()
ruleStr.append(Rule.getRuleMatch(item.relation.rule.filed, item.relation.rule.check, item.relation.rule.value, item.relation.rule.simSlot)).append(item.relation.sender.name)
val detailStr = StringBuilder()
detailStr.append(ResUtils.getString(R.string.from)).append(item.logs.from).append("\n\n")
detailStr.append(ResUtils.getString(R.string.msg)).append(item.logs.content).append("\n\n")
if (!TextUtils.isEmpty(item.logs.simInfo)) detailStr.append(ResUtils.getString(R.string.slot)).append(item.logs.simInfo).append("\n\n")
detailStr.append(ResUtils.getString(R.string.rule)).append(ruleStr.toString()).append("\n\n")
@SuppressLint("SimpleDateFormat") val utcFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
detailStr.append(ResUtils.getString(R.string.time)).append(DateUtils.date2String(item.logs.time, utcFormatter)).append("\n\n")
detailStr.append(ResUtils.getString(R.string.result)).append(FORWARD_STATUS_MAP[item.logs.forwardStatus]).append("\n--------------------\n").append(item.logs.forwardResponse)
MaterialDialog.Builder(requireContext())
.iconRes(item.logs.simImageId)
.title(R.string.details)
.content(detailStr.toString())
.cancelable(true)
.positiveText(R.string.del)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(item.logs.id)
XToastUtils.success(R.string.delete_log_toast)
}
.negativeText(R.string.resend)
.onNegative { _: MaterialDialog?, _: DialogAction? ->
XToastUtils.toast(R.string.resend_toast)
SendUtils.resendMsg(item, false)
}
.neutralText(R.string.rematch)
.neutralColor(ResUtils.getColors(R.color.red))
.onNeutral { _: MaterialDialog?, _: DialogAction? ->
XToastUtils.toast(R.string.rematch_toast)
SendUtils.resendMsg(item, true)
}
.show()
}
override fun onItemRemove(view: View?, id: Int) {}
package com.idormy.sms.forwarder.fragment
import android.annotation.SuppressLint
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.MsgPagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.MsgViewModel
import com.idormy.sms.forwarder.databinding.FragmentLogsBinding
import com.idormy.sms.forwarder.utils.EVENT_UPDATE_LOGS_TYPE
import com.idormy.sms.forwarder.utils.FORWARD_STATUS_MAP
import com.idormy.sms.forwarder.utils.XToastUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.utils.ResUtils
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.xutil.data.DateUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@Suppress("PropertyName")
@Page(name = "转发日志")
class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnItemClickListener {
val TAG: String = LogsFragment::class.java.simpleName
private var adapter = MsgPagingAdapter(this)
private val viewModel by viewModels<MsgViewModel> { BaseViewModelFactory(context) }
private var currentType: String = "sms"
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentLogsBinding {
return FragmentLogsBinding.inflate(inflater, container, false)
}
/**
* @return 返回为 null意为不需要导航栏
*/
override fun initTitle(): TitleBar? {
return null
}
/**
* 初始化控件
*/
override fun initViews() {
val virtualLayoutManager = VirtualLayoutManager(requireContext())
binding!!.recyclerView.layoutManager = virtualLayoutManager
val viewPool = RecycledViewPool()
binding!!.recyclerView.setRecycledViewPool(viewPool)
viewPool.setMaxRecycledViews(0, 10)
binding!!.recyclerView.isFocusableInTouchMode = false
binding!!.tabBar.setTabTitles(ResUtils.getStringArray(R.array.type_param_option))
binding!!.tabBar.setOnTabClickListener { _, position ->
//XToastUtils.toast("点击了$title--$position")
currentType = when (position) {
1 -> "call"
2 -> "app"
else -> "sms"
}
viewModel.setType(currentType)
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).post(currentType)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
}
}
override fun initListeners() {
binding!!.recyclerView.adapter = adapter
//下拉刷新
binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout ->
//adapter.refresh()
lifecycleScope.launch {
viewModel.setType(currentType).allMsg.collectLatest { adapter.submitData(it) }
}
refreshLayout.finishRefresh()
}
binding!!.refreshLayout.autoRefresh()
}
override fun onItemClicked(view: View?, item: MsgAndLogs) {
Log.d(TAG, "item: $item")
val detailStr = StringBuilder()
detailStr.append(ResUtils.getString(R.string.from)).append(item.msg.from).append("\n\n")
detailStr.append(ResUtils.getString(R.string.msg)).append(item.msg.content).append("\n\n")
if (!TextUtils.isEmpty(item.msg.simInfo)) detailStr.append(ResUtils.getString(R.string.slot)).append(item.msg.simInfo).append("\n\n")
@SuppressLint("SimpleDateFormat") val utcFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
detailStr.append(ResUtils.getString(R.string.time)).append(DateUtils.date2String(item.msg.time, utcFormatter))
MaterialDialog.Builder(requireContext())
.iconRes(item.msg.simImageId)
.title(R.string.details)
.content(detailStr.toString())
.cancelable(true)
.positiveText(R.string.del)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(item.msg.id)
XToastUtils.success(R.string.delete_log_toast)
}
.neutralText(R.string.rematch)
.neutralColor(ResUtils.getColors(R.color.red))
.onNeutral { _: MaterialDialog?, _: DialogAction? ->
XToastUtils.toast(R.string.rematch_toast)
//SendUtils.resendMsg(item, true)
}
.show()
}
override fun onLogsClicked(view: View?, item: LogsDetail) {
Log.d(TAG, "item: $item")
val ruleStr = StringBuilder()
ruleStr.append(Rule.getRuleMatch(item.ruleFiled, item.ruleCheck, item.ruleValue, item.ruleSimSlot)).append(item.senderName)
val detailStr = StringBuilder()
detailStr.append(ResUtils.getString(R.string.rule)).append(ruleStr.toString()).append("\n\n")
@SuppressLint("SimpleDateFormat") val utcFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
detailStr.append(ResUtils.getString(R.string.time)).append(DateUtils.date2String(item.time, utcFormatter)).append("\n\n")
detailStr.append(ResUtils.getString(R.string.result)).append(FORWARD_STATUS_MAP[item.forwardStatus]).append("\n--------------------\n").append(item.forwardResponse)
MaterialDialog.Builder(requireContext())
.title(R.string.details)
.content(detailStr.toString())
.cancelable(true)
.positiveText(R.string.del)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(item.id)
XToastUtils.success(R.string.delete_log_toast)
}
.negativeText(R.string.resend)
.onNegative { _: MaterialDialog?, _: DialogAction? ->
XToastUtils.toast(R.string.resend_toast)
//SendUtils.resendMsg(item, false)
}
.show()
}
override fun onItemRemove(view: View?, id: Int) {}
}

@ -1,131 +1,129 @@
package com.idormy.sms.forwarder.fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.RulePagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.RuleViewModel
import com.idormy.sms.forwarder.databinding.FragmentRulesBinding
import com.idormy.sms.forwarder.utils.*
import com.jeremyliao.liveeventbus.LiveEventBus
import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Suppress("PropertyName")
@Page(name = "转发规则")
class RulesFragment : BaseFragment<FragmentRulesBinding?>(), RulePagingAdapter.OnItemClickListener {
val TAG: String = RulesFragment::class.java.simpleName
private var adapter = RulePagingAdapter(this)
private val viewModel by viewModels<RuleViewModel> { BaseViewModelFactory(context) }
private var currentType: String = "sms"
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentRulesBinding {
return FragmentRulesBinding.inflate(inflater, container, false)
}
/**
* @return 返回为 null意为不需要导航栏
*/
override fun initTitle(): TitleBar? {
return null
}
/**
* 初始化控件
*/
override fun initViews() {
val virtualLayoutManager = VirtualLayoutManager(requireContext())
binding!!.recyclerView.layoutManager = virtualLayoutManager
val viewPool = RecycledViewPool()
binding!!.recyclerView.setRecycledViewPool(viewPool)
viewPool.setMaxRecycledViews(0, 10)
binding!!.tabBar.setTabTitles(ResUtils.getStringArray(R.array.type_param_option))
binding!!.tabBar.setOnTabClickListener { _, position ->
//XToastUtils.toast("点击了$title--$position")
currentType = when (position) {
1 -> "call"
2 -> "app"
else -> "sms"
}
viewModel.setType(currentType)
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).post(currentType)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
}
}
override fun initListeners() {
binding!!.recyclerView.adapter = adapter
//下拉刷新
binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout ->
refreshLayout.layout.postDelayed({
//adapter!!.refresh()
lifecycleScope.launch {
viewModel.setType(currentType).allRules.collectLatest { adapter.submitData(it) }
}
refreshLayout.finishRefresh()
}, 200)
}
binding!!.refreshLayout.autoRefresh()
}
override fun onItemClicked(view: View?, item: RuleAndSender) {
Log.e(TAG, item.toString())
when (view?.id) {
R.id.iv_copy -> {
PageOption.to(RulesEditFragment::class.java)
.setNewActivity(true)
.putLong(KEY_RULE_ID, item.rule.id)
.putString(KEY_RULE_TYPE, item.rule.type)
.putBoolean(KEY_RULE_CLONE, true)
.open(this)
}
R.id.iv_edit -> {
PageOption.to(RulesEditFragment::class.java)
.setNewActivity(true)
.putLong(KEY_RULE_ID, item.rule.id)
.putString(KEY_RULE_TYPE, item.rule.type)
.open(this)
}
R.id.iv_delete -> {
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(item.rule.id)
XToastUtils.success(R.string.delete_rule_toast)
}
.show()
}
else -> {}
}
}
override fun onItemRemove(view: View?, id: Int) {}
package com.idormy.sms.forwarder.fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.RulePagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.RuleViewModel
import com.idormy.sms.forwarder.databinding.FragmentRulesBinding
import com.idormy.sms.forwarder.utils.*
import com.jeremyliao.liveeventbus.LiveEventBus
import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Suppress("PropertyName")
@Page(name = "转发规则")
class RulesFragment : BaseFragment<FragmentRulesBinding?>(), RulePagingAdapter.OnItemClickListener {
val TAG: String = RulesFragment::class.java.simpleName
private var adapter = RulePagingAdapter(this)
private val viewModel by viewModels<RuleViewModel> { BaseViewModelFactory(context) }
private var currentType: String = "sms"
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentRulesBinding {
return FragmentRulesBinding.inflate(inflater, container, false)
}
/**
* @return 返回为 null意为不需要导航栏
*/
override fun initTitle(): TitleBar? {
return null
}
/**
* 初始化控件
*/
override fun initViews() {
val virtualLayoutManager = VirtualLayoutManager(requireContext())
binding!!.recyclerView.layoutManager = virtualLayoutManager
val viewPool = RecycledViewPool()
binding!!.recyclerView.setRecycledViewPool(viewPool)
viewPool.setMaxRecycledViews(0, 10)
binding!!.tabBar.setTabTitles(ResUtils.getStringArray(R.array.type_param_option))
binding!!.tabBar.setOnTabClickListener { _, position ->
//XToastUtils.toast("点击了$title--$position")
currentType = when (position) {
1 -> "call"
2 -> "app"
else -> "sms"
}
viewModel.setType(currentType)
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).post(currentType)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
}
}
override fun initListeners() {
binding!!.recyclerView.adapter = adapter
//下拉刷新
binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout ->
refreshLayout.layout.postDelayed({
//adapter!!.refresh()
lifecycleScope.launch {
viewModel.setType(currentType).allRules.collectLatest { adapter.submitData(it) }
}
refreshLayout.finishRefresh()
}, 200)
}
binding!!.refreshLayout.autoRefresh()
}
override fun onItemClicked(view: View?, item: Rule) {
when (view?.id) {
R.id.iv_copy -> {
PageOption.to(RulesEditFragment::class.java)
.setNewActivity(true)
.putLong(KEY_RULE_ID, item.id)
.putString(KEY_RULE_TYPE, item.type)
.putBoolean(KEY_RULE_CLONE, true)
.open(this)
}
R.id.iv_edit -> {
PageOption.to(RulesEditFragment::class.java)
.setNewActivity(true)
.putLong(KEY_RULE_ID, item.id)
.putString(KEY_RULE_TYPE, item.type)
.open(this)
}
R.id.iv_delete -> {
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(item.id)
XToastUtils.success(R.string.delete_rule_toast)
}
.show()
}
else -> {}
}
}
override fun onItemRemove(view: View?, id: Int) {}
}

@ -1,141 +1,141 @@
package com.idormy.sms.forwarder.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import android.util.Log
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.BatteryUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.Worker
import com.idormy.sms.forwarder.workers.SendWorker
import java.util.*
@Suppress("DEPRECATION")
class BatteryService : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate--------------")
//纯客户端模式
//if (SettingUtils.enablePureClientMode) return
val batteryFilter = IntentFilter()
batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryReceiver, batteryFilter)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand--------------")
return START_STICKY
}
override fun onDestroy() {
Log.i(TAG, "onDestroy--------------")
super.onDestroy()
//纯客户端模式
//if (SettingUtils.enablePureClientMode) return
unregisterReceiver(batteryReceiver)
}
// 接收电池信息更新的广播
private val batteryReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@SuppressLint("DefaultLocale")
override fun onReceive(context: Context, intent: Intent) {
//自动删除N天前的转发记录
if (SettingUtils.autoCleanLogsDays > 0) {
Log.d(TAG, "自动删除N天前的转发记录")
val cal = Calendar.getInstance()
cal.add(Calendar.DAY_OF_MONTH, 0 - SettingUtils.autoCleanLogsDays)
Core.logs.deleteTimeAgo(cal.timeInMillis)
}
//电量发生变化
val levelCur: Int = intent.getIntExtra("level", 0)
val levelPre: Int = SettingUtils.batteryLevelCurrent
if (levelCur != levelPre) {
var msg: String = BatteryUtils.getBatteryInfo(intent).toString()
SettingUtils.batteryLevelCurrent = levelCur
val levelMin: Int = SettingUtils.batteryLevelMin
val levelMax: Int = SettingUtils.batteryLevelMax
if (SettingUtils.batteryLevelOnce && levelMin > 0 && levelPre > levelCur && levelCur <= levelMin) { //电量下降到下限
msg = String.format(getString(R.string.below_level_min), msg)
sendMessage(context, msg)
return
} else if (SettingUtils.batteryLevelOnce && levelMax > 0 && levelPre < levelCur && levelCur >= levelMax) { //电量上升到上限
msg = String.format(getString(R.string.over_level_max), msg)
sendMessage(context, msg)
return
} else if (!SettingUtils.batteryLevelOnce && levelMin > 0 && levelPre > levelCur && levelCur == levelMin) { //电量下降到下限
msg = String.format(getString(R.string.reach_level_min), msg)
sendMessage(context, msg)
return
} else if (!SettingUtils.batteryLevelOnce && levelMax > 0 && levelPre < levelCur && levelCur == levelMax) { //电量上升到上限
msg = String.format(getString(R.string.reach_level_max), msg)
sendMessage(context, msg)
return
}
}
//充电状态改变
val status: Int = intent.getIntExtra("status", 0)
if (SettingUtils.enableBatteryReceiver) {
val oldStatus: Int = SettingUtils.batteryStatus
if (status != oldStatus) {
var msg: String = BatteryUtils.getBatteryInfo(intent).toString()
SettingUtils.batteryStatus = status
msg = getString(R.string.battery_status_changed) + BatteryUtils.getStatus(
oldStatus
) + "" + BatteryUtils.getStatus(status) + msg
sendMessage(context, msg)
}
}
}
}
//发送信息
private fun sendMessage(context: Context, msg: String) {
Log.i(TAG, msg)
try {
val msgInfo = MsgInfo(
"app",
"88888888",
msg,
Date(),
getString(R.string.battery_status_monitor),
-1
)
val request = OneTimeWorkRequestBuilder<SendWorker>()
.setInputData(
workDataOf(
Worker.sendMsgInfo to Gson().toJson(msgInfo),
)
)
.build()
WorkManager.getInstance(context).enqueue(request)
} catch (e: Exception) {
Log.e(TAG, "getLog e:" + e.message)
}
}
companion object {
private const val TAG = "BatteryReceiver"
}
package com.idormy.sms.forwarder.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import android.util.Log
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.BatteryUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.Worker
import com.idormy.sms.forwarder.workers.SendWorker
import java.util.*
@Suppress("DEPRECATION")
class BatteryService : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate--------------")
//纯客户端模式
//if (SettingUtils.enablePureClientMode) return
val batteryFilter = IntentFilter()
batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryReceiver, batteryFilter)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand--------------")
return START_STICKY
}
override fun onDestroy() {
Log.i(TAG, "onDestroy--------------")
super.onDestroy()
//纯客户端模式
//if (SettingUtils.enablePureClientMode) return
unregisterReceiver(batteryReceiver)
}
// 接收电池信息更新的广播
private val batteryReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@SuppressLint("DefaultLocale")
override fun onReceive(context: Context, intent: Intent) {
//自动删除N天前的转发记录
if (SettingUtils.autoCleanLogsDays > 0) {
Log.d(TAG, "自动删除N天前的转发记录")
val cal = Calendar.getInstance()
cal.add(Calendar.DAY_OF_MONTH, 0 - SettingUtils.autoCleanLogsDays)
Core.msg.deleteTimeAgo(cal.timeInMillis)
}
//电量发生变化
val levelCur: Int = intent.getIntExtra("level", 0)
val levelPre: Int = SettingUtils.batteryLevelCurrent
if (levelCur != levelPre) {
var msg: String = BatteryUtils.getBatteryInfo(intent).toString()
SettingUtils.batteryLevelCurrent = levelCur
val levelMin: Int = SettingUtils.batteryLevelMin
val levelMax: Int = SettingUtils.batteryLevelMax
if (SettingUtils.batteryLevelOnce && levelMin > 0 && levelPre > levelCur && levelCur <= levelMin) { //电量下降到下限
msg = String.format(getString(R.string.below_level_min), msg)
sendMessage(context, msg)
return
} else if (SettingUtils.batteryLevelOnce && levelMax > 0 && levelPre < levelCur && levelCur >= levelMax) { //电量上升到上限
msg = String.format(getString(R.string.over_level_max), msg)
sendMessage(context, msg)
return
} else if (!SettingUtils.batteryLevelOnce && levelMin > 0 && levelPre > levelCur && levelCur == levelMin) { //电量下降到下限
msg = String.format(getString(R.string.reach_level_min), msg)
sendMessage(context, msg)
return
} else if (!SettingUtils.batteryLevelOnce && levelMax > 0 && levelPre < levelCur && levelCur == levelMax) { //电量上升到上限
msg = String.format(getString(R.string.reach_level_max), msg)
sendMessage(context, msg)
return
}
}
//充电状态改变
val status: Int = intent.getIntExtra("status", 0)
if (SettingUtils.enableBatteryReceiver) {
val oldStatus: Int = SettingUtils.batteryStatus
if (status != oldStatus) {
var msg: String = BatteryUtils.getBatteryInfo(intent).toString()
SettingUtils.batteryStatus = status
msg = getString(R.string.battery_status_changed) + BatteryUtils.getStatus(
oldStatus
) + "" + BatteryUtils.getStatus(status) + msg
sendMessage(context, msg)
}
}
}
}
//发送信息
private fun sendMessage(context: Context, msg: String) {
Log.i(TAG, msg)
try {
val msgInfo = MsgInfo(
"app",
"88888888",
msg,
Date(),
getString(R.string.battery_status_monitor),
-1
)
val request = OneTimeWorkRequestBuilder<SendWorker>()
.setInputData(
workDataOf(
Worker.sendMsgInfo to Gson().toJson(msgInfo),
)
)
.build()
WorkManager.getInstance(context).enqueue(request)
} catch (e: Exception) {
Log.e(TAG, "getLog e:" + e.message)
}
}
companion object {
private const val TAG = "BatteryReceiver"
}
}

@ -12,6 +12,9 @@ object Worker {
const val sendLogId = "send_log_id"
const val sendSbnId = "send_sbn_id"
const val updateLogs = "update_logs"
const val ruleId = "rule_id"
const val senderIndex = "sender_index"
const val msgId = "msg_id"
}
//初始化相关
@ -103,6 +106,11 @@ const val CHECK_REGEX = "regex"
const val CHECK_SIM_SLOT_ALL = "ALL"
const val CHECK_SIM_SLOT_1 = "SIM1"
const val CHECK_SIM_SLOT_2 = "SIM2"
//发送通道执行逻辑ALL=全部执行, UntilFail=失败即终止, UntilSuccess=成功即终止
const val SENDER_LOGIC_ALL = "ALL"
const val SENDER_LOGIC_UNTIL_FAIL = "UntilFail"
const val SENDER_LOGIC_UNTIL_SUCCESS = "UntilSuccess"
val TYPE_MAP = object : HashMap<String, String>() {
init {
put("sms", getString(R.string.rule_sms))

@ -187,9 +187,11 @@ class PhoneUtils private constructor() {
Log.d(TAG, "selectionArgs = $selectionArgs")
//为了兼容性这里全部取出后手动分页
val cursor = Core.app.contentResolver.query(
val cursor = (if (limit == 1) Core.app.contentResolver.query(
CallLog.Calls.CONTENT_URI, null, selection, selectionArgs.toTypedArray(), CallLog.Calls.DEFAULT_SORT_ORDER + " limit $limit offset $offset"
) else Core.app.contentResolver.query(
CallLog.Calls.CONTENT_URI, null, selection, selectionArgs.toTypedArray(), CallLog.Calls.DEFAULT_SORT_ORDER // + " limit $limit offset $offset"
) ?: return callInfoList
)) ?: return callInfoList
Log.i(TAG, "cursor count:" + cursor.count)
// 避免超过总数后循环取出

@ -0,0 +1,67 @@
@file:Suppress("DEPRECATION")
package com.idormy.sms.forwarder.utils
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
/**
* Created by aykutasil on 8.12.2016.
*/
@Suppress("unused")
class PrefsHelper private constructor() {
lateinit var preference: SharedPreferences
val prefEditor: SharedPreferences.Editor
get() = preference.edit()
constructor(context: Context, prefName: String) : this() {
preference = context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
}
constructor(context: Context) : this() {
preference = getDefaultPreference(context)
}
companion object {
private val DEFAULT_STRING_VALUE: String? = null
private const val DEFAULT_INT_VALUE = 0
private const val DEFAULT_BOOLEAN_VALUE = false
fun getDefaultPreference(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
fun writePrefString(context: Context, key: String, value: String?) {
PrefsHelper(context).prefEditor.putString(key, value).commit()
}
fun readPrefString(context: Context, key: String): String? {
return PrefsHelper(context).preference.getString(key, DEFAULT_STRING_VALUE)
}
fun writePrefInt(context: Context, key: String, value: Int) {
PrefsHelper(context).prefEditor.putInt(key, value).commit()
}
fun readPrefInt(context: Context, key: String): Int {
return PrefsHelper(context).preference.getInt(key, DEFAULT_INT_VALUE)
}
fun writePrefBool(context: Context, key: String, value: Boolean) {
PrefsHelper(context).prefEditor.putBoolean(key, value).commit()
}
fun readPrefBool(context: Context, key: String): Boolean {
return PrefsHelper(context).preference.getBoolean(key, DEFAULT_BOOLEAN_VALUE)
}
fun clearPreference(context: Context) {
PrefsHelper(context).preference.edit().clear().apply()
}
}
}

@ -9,11 +9,11 @@ import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.result.SendResponse
import com.idormy.sms.forwarder.entity.setting.*
import com.idormy.sms.forwarder.utils.sender.*
import com.idormy.sms.forwarder.workers.SendLogicWorker
import com.idormy.sms.forwarder.workers.SendWorker
import com.idormy.sms.forwarder.workers.UpdateLogsWorker
import com.xuexiang.xui.utils.ResUtils
@ -24,13 +24,6 @@ import java.util.*
object SendUtils {
private const val TAG = "SendUtils"
//批量发送消息
/*fun sendMsgList(infoList: List<MsgInfo>, type: String) {
for (msgInfo in infoList) {
sendMsg(msgInfo, type)
}
}*/
//发送消息
fun sendMsg(msgInfo: MsgInfo) {
val request = OneTimeWorkRequestBuilder<SendWorker>()
@ -53,9 +46,9 @@ object SendUtils {
e.printStackTrace()
Date()
}
val simInfo: String = item.logs.simInfo
val simInfo: String = item.msg.simInfo
val simSlot: Int = if (simInfo.startsWith("SIM2")) 2 else 1
val msgInfo = MsgInfo(item.logs.type, item.logs.from, item.logs.content, date, simInfo, simSlot)
val msgInfo = MsgInfo(item.msg.type, item.msg.from, item.msg.content, date, simInfo, simSlot)
Log.d(TAG, "resendMsg msgInfo:$msgInfo")
if (rematch) {
@ -63,72 +56,73 @@ object SendUtils {
return
}
sendMsgSender(msgInfo, item.relation.rule, item.relation.sender, item.logs.id)
//sendMsgSender(msgInfo, item.rule, item.sender, item.logs.id)
}
//匹配发送通道发送消息
fun sendMsgSender(msgInfo: MsgInfo, rule: Rule, sender: Sender, logId: Long) {
fun sendMsgSender(msgInfo: MsgInfo, rule: Rule, senderIndex: Int = 0, logId: Long = 0L, msgId: Long = 0L) {
try {
val sender = rule.senderList[senderIndex]
when (sender.type) {
TYPE_DINGTALK_GROUP_ROBOT -> {
val settingVo = Gson().fromJson(sender.jsonSetting, DingtalkGroupRobotSetting::class.java)
DingtalkGroupRobotUtils.sendMsg(settingVo, msgInfo, rule, logId)
DingtalkGroupRobotUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_EMAIL -> {
val settingVo = Gson().fromJson(sender.jsonSetting, EmailSetting::class.java)
EmailUtils.sendMsg(settingVo, msgInfo, rule, logId)
EmailUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_BARK -> {
val settingVo = Gson().fromJson(sender.jsonSetting, BarkSetting::class.java)
BarkUtils.sendMsg(settingVo, msgInfo, rule, logId)
BarkUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_WEBHOOK -> {
val settingVo = Gson().fromJson(sender.jsonSetting, WebhookSetting::class.java)
WebhookUtils.sendMsg(settingVo, msgInfo, rule, logId)
WebhookUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_WEWORK_ROBOT -> {
val settingVo = Gson().fromJson(sender.jsonSetting, WeworkRobotSetting::class.java)
WeworkRobotUtils.sendMsg(settingVo, msgInfo, rule, logId)
WeworkRobotUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_WEWORK_AGENT -> {
val settingVo = Gson().fromJson(sender.jsonSetting, WeworkAgentSetting::class.java)
WeworkAgentUtils.sendMsg(settingVo, msgInfo, rule, logId)
WeworkAgentUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_SERVERCHAN -> {
val settingVo = Gson().fromJson(sender.jsonSetting, ServerchanSetting::class.java)
ServerchanUtils.sendMsg(settingVo, msgInfo, rule, logId)
ServerchanUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_TELEGRAM -> {
val settingVo = Gson().fromJson(sender.jsonSetting, TelegramSetting::class.java)
TelegramUtils.sendMsg(settingVo, msgInfo, rule, logId)
TelegramUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_SMS -> {
val settingVo = Gson().fromJson(sender.jsonSetting, SmsSetting::class.java)
SmsUtils.sendMsg(settingVo, msgInfo, rule, logId)
SmsUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_FEISHU -> {
val settingVo = Gson().fromJson(sender.jsonSetting, FeishuSetting::class.java)
FeishuUtils.sendMsg(settingVo, msgInfo, rule, logId)
FeishuUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_PUSHPLUS -> {
val settingVo = Gson().fromJson(sender.jsonSetting, PushplusSetting::class.java)
PushplusUtils.sendMsg(settingVo, msgInfo, rule, logId)
PushplusUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_GOTIFY -> {
val settingVo = Gson().fromJson(sender.jsonSetting, GotifySetting::class.java)
GotifyUtils.sendMsg(settingVo, msgInfo, rule, logId)
GotifyUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_DINGTALK_INNER_ROBOT -> {
val settingVo = Gson().fromJson(sender.jsonSetting, DingtalkInnerRobotSetting::class.java)
DingtalkInnerRobotUtils.sendMsg(settingVo, msgInfo, rule, logId)
DingtalkInnerRobotUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_FEISHU_APP -> {
val settingVo = Gson().fromJson(sender.jsonSetting, FeishuAppSetting::class.java)
FeishuAppUtils.sendMsg(settingVo, msgInfo, rule, logId)
FeishuAppUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_URL_SCHEME -> {
val settingVo = Gson().fromJson(sender.jsonSetting, UrlSchemeSetting::class.java)
UrlSchemeUtils.sendMsg(settingVo, msgInfo, rule, logId)
UrlSchemeUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
else -> {
updateLogs(logId, 0, "未知发送通道")
@ -140,6 +134,24 @@ object SendUtils {
}
}
fun senderLogic(status: Int, msgInfo: MsgInfo, rule: Rule?, senderIndex: Int = 0, msgId: Long = 0L) {
if (rule == null) return
//发送通道执行逻辑ALL=全部执行, UntilFail=失败即终止, UntilSuccess=成功即终止
if (senderIndex < rule.senderList.count() - 1 && ((status == 2 && rule.senderLogic == SENDER_LOGIC_UNTIL_FAIL) || (status == 0 && rule.senderLogic == SENDER_LOGIC_UNTIL_SUCCESS))) {
val request = OneTimeWorkRequestBuilder<SendLogicWorker>()
.setInputData(
workDataOf(
Worker.sendMsgInfo to Gson().toJson(msgInfo),
Worker.ruleId to rule.id,
Worker.senderIndex to senderIndex + 1,
Worker.msgId to msgId,
)
)
.build()
WorkManager.getInstance(XUtil.getContext()).enqueue(request)
}
}
//更新转发日志状态
fun updateLogs(logId: Long?, status: Int, response: String) {

@ -25,7 +25,9 @@ class BarkUtils {
setting: BarkSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val title: String = if (rule != null) {
msgInfo.getTitleForSend(setting.title.toString(), rule.regexReplace)
@ -86,18 +88,19 @@ class BarkUtils {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, BarkResult::class.java)
if (resp?.code == 200L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.code == 200L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -105,7 +108,7 @@ class BarkUtils {
}
fun sendMsg(setting: BarkSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -30,7 +30,9 @@ class DingtalkGroupRobotUtils private constructor() {
setting: DingtalkGroupRobotSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val content: String = if (rule != null) {
msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace)
@ -97,18 +99,18 @@ class DingtalkGroupRobotUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, DingtalkResult::class.java)
if (resp?.errcode == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.errcode == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -116,7 +118,7 @@ class DingtalkGroupRobotUtils private constructor() {
}
fun sendMsg(setting: DingtalkGroupRobotSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -36,12 +36,14 @@ class DingtalkInnerRobotUtils private constructor() {
setting: DingtalkInnerRobotSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
var accessToken: String by SharedPreference("accessToken_" + setting.agentID, "")
var expiresIn: Long by SharedPreference("expiresIn_" + setting.agentID, 0L)
if (!TextUtils.isEmpty(accessToken) && expiresIn > System.currentTimeMillis()) {
return sendTextMsg(setting, msgInfo, rule, logId)
return sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
}
val requestUrl = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
@ -93,7 +95,9 @@ class DingtalkInnerRobotUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
@ -103,9 +107,10 @@ class DingtalkInnerRobotUtils private constructor() {
if (!TextUtils.isEmpty(resp?.accessToken)) {
accessToken = resp.accessToken.toString()
expiresIn = System.currentTimeMillis() + ((resp.expireIn ?: 7200) - 120) * 1000L //提前2分钟过期
sendTextMsg(setting, msgInfo, rule, logId)
sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
} else {
SendUtils.updateLogs(logId, 0, String.format(getString(R.string.request_failed_tips), response))
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
@ -118,7 +123,9 @@ class DingtalkInnerRobotUtils private constructor() {
setting: DingtalkInnerRobotSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val requestUrl = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend"
Log.d(TAG, "requestUrl$requestUrl")
@ -197,25 +204,25 @@ class DingtalkInnerRobotUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, DingtalkInnerRobotResult::class.java)
if (!TextUtils.isEmpty(resp?.processQueryKey)) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (!TextUtils.isEmpty(resp?.processQueryKey)) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
}
fun sendMsg(setting: DingtalkInnerRobotSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}

@ -21,7 +21,9 @@ class EmailUtils {
setting: EmailSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val title: String = if (rule != null) {
msgInfo.getTitleForSend(setting.title.toString(), rule.regexReplace)
@ -134,18 +136,21 @@ class EmailUtils {
MailSender.getInstance().sendMail(mail, object : MailSender.OnMailSendListener {
override fun onError(e: Throwable) {
Log.e("MailSender", e.message.toString())
SendUtils.updateLogs(logId, 0, e.message.toString())
val status = 0
SendUtils.updateLogs(logId, status, e.message.toString())
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess() {
SendUtils.updateLogs(logId, 2, ResUtils.getString(R.string.request_succeeded))
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
}
})
}
fun sendMsg(setting: EmailSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -28,13 +28,15 @@ class FeishuAppUtils private constructor() {
setting: FeishuAppSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
var accessToken: String by SharedPreference("feishu_access_token_" + setting.appId, "")
var expiresIn: Long by SharedPreference("feishu_expires_in_" + setting.appId, 0L)
if (!TextUtils.isEmpty(accessToken) && expiresIn > System.currentTimeMillis()) {
return sendTextMsg(setting, msgInfo, rule, logId)
return sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
}
val requestUrl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
@ -51,7 +53,9 @@ class FeishuAppUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
@ -61,9 +65,10 @@ class FeishuAppUtils private constructor() {
if (!TextUtils.isEmpty(resp?.tenant_access_token)) {
accessToken = resp.tenant_access_token.toString()
expiresIn = System.currentTimeMillis() + ((resp.expire ?: 7010) - 120) * 1000L //提前2分钟过期
sendTextMsg(setting, msgInfo, rule, logId)
sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
} else {
SendUtils.updateLogs(logId, 0, String.format(getString(R.string.request_failed_tips), response))
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
@ -76,7 +81,9 @@ class FeishuAppUtils private constructor() {
setting: FeishuAppSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val requestUrl = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=user_id"
Log.d(TAG, "requestUrl$requestUrl")
@ -117,7 +124,9 @@ class FeishuAppUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
@ -126,18 +135,16 @@ class FeishuAppUtils private constructor() {
//Log.d(TAG, "cipherSuite=" + response.handshake().cipherSuite().toString())
val resp = Gson().fromJson(response, FeishuAppResult::class.java)
if (resp?.code == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.code == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
}
fun sendMsg(setting: FeishuAppSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
private fun jsonInnerStr(string: String?): String {

@ -83,7 +83,9 @@ class FeishuUtils private constructor() {
setting: FeishuSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val from: String = msgInfo.from
val title: String = if (rule != null) {
@ -144,18 +146,18 @@ class FeishuUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, FeishuResult::class.java)
if (resp?.code == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.code == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -181,7 +183,7 @@ class FeishuUtils private constructor() {
}
fun sendMsg(setting: FeishuSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -23,7 +23,9 @@ class GotifyUtils {
setting: GotifySetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val title: String = if (rule != null) {
msgInfo.getTitleForSend(setting.title.toString(), rule.regexReplace)
@ -64,18 +66,18 @@ class GotifyUtils {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, GotifyResult::class.java)
if (resp?.id != null) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.id != null) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -83,7 +85,7 @@ class GotifyUtils {
}
fun sendMsg(setting: GotifySetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -27,7 +27,9 @@ class PushplusUtils private constructor() {
setting: PushplusSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val title: String = if (rule != null) {
msgInfo.getTitleForSend(setting.titleTemplate.toString(), rule.regexReplace)
@ -79,18 +81,18 @@ class PushplusUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, PushplusResult::class.java)
if (resp?.code == 200L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.code == 200L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -98,7 +100,7 @@ class PushplusUtils private constructor() {
}
fun sendMsg(setting: PushplusSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -24,7 +24,9 @@ class ServerchanUtils {
setting: ServerchanSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val title: String = if (rule != null) {
msgInfo.getTitleForSend(setting.titleTemplate.toString(), rule.regexReplace)
@ -58,18 +60,17 @@ class ServerchanUtils {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, ServerchanResult::class.java)
if (resp?.code == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.code == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -77,7 +78,7 @@ class ServerchanUtils {
}
fun sendMsg(setting: ServerchanSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -26,16 +26,20 @@ class SmsUtils {
setting: SmsSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
//仅当无网络时启用 && 判断是否真实有网络
if (setting.onlyNoNetwork == true && NetworkUtils.isHaveInternet() && NetworkUtils.isAvailableByPing()) {
SendUtils.updateLogs(logId, 0, ResUtils.getString(R.string.OnlyNoNetwork))
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
return
}
if (ActivityCompat.checkSelfPermission(XUtil.getContext(), Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
SendUtils.updateLogs(logId, 0, ResUtils.getString(R.string.no_sms_sending_permission))
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
return
}
@ -62,13 +66,15 @@ class SmsUtils {
val res: String? = PhoneUtils.sendSms(mSubscriptionId, mobiles, content)
if (res == null) {
SendUtils.updateLogs(logId, 2, ResUtils.getString(R.string.request_succeeded))
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
} else {
SendUtils.updateLogs(logId, 0, res)
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
fun sendMsg(setting: SmsSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -30,7 +30,9 @@ class TelegramUtils private constructor() {
setting: TelegramSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
if (setting.method == null || setting.method == "POST") {
msgInfo.content = htmlEncode(msgInfo.content)
@ -116,18 +118,18 @@ class TelegramUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, TelegramResult::class.java)
if (resp?.ok == true) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.ok == true) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -135,7 +137,7 @@ class TelegramUtils private constructor() {
}
fun sendMsg(setting: TelegramSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
private fun htmlEncode(source: String?): String {

@ -26,7 +26,9 @@ class UrlSchemeUtils private constructor() {
setting: UrlSchemeSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val from: String = msgInfo.from
val content: String = if (rule != null) {
@ -62,16 +64,18 @@ class UrlSchemeUtils private constructor() {
try {
XUtil.getContext().startActivity(intent)
SendUtils.updateLogs(logId, 2, "调用成功")
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, e.message.toString())
SendUtils.updateLogs(logId, 0, e.message.toString())
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
fun sendMsg(setting: UrlSchemeSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -32,7 +32,9 @@ class WebhookUtils {
setting: WebhookSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val from: String = msgInfo.from
val content: String = if (rule != null) {
@ -183,12 +185,15 @@ class WebhookUtils {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
SendUtils.updateLogs(logId, 2, response)
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
}
})
@ -203,7 +208,7 @@ class WebhookUtils {
}
fun sendMsg(setting: WebhookSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -36,13 +36,15 @@ class WeworkAgentUtils private constructor() {
setting: WeworkAgentSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
var accessToken: String by SharedPreference("access_token_" + setting.agentID, "")
var expiresIn: Long by SharedPreference("expires_in_" + setting.agentID, 0L)
if (!TextUtils.isEmpty(accessToken) && expiresIn > System.currentTimeMillis()) {
return sendTextMsg(setting, msgInfo, rule, logId)
return sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
}
var getTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
@ -90,7 +92,9 @@ class WeworkAgentUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
@ -100,9 +104,10 @@ class WeworkAgentUtils private constructor() {
if (resp?.errcode == 0L) {
accessToken = resp.access_token.toString()
expiresIn = System.currentTimeMillis() + ((resp.expires_in ?: 7200) - 120) * 1000L //提前2分钟过期
sendTextMsg(setting, msgInfo, rule, logId)
sendTextMsg(setting, msgInfo, rule, senderIndex, logId, msgId)
} else {
SendUtils.updateLogs(logId, 0, String.format(getString(R.string.request_failed_tips), response))
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
@ -115,7 +120,9 @@ class WeworkAgentUtils private constructor() {
setting: WeworkAgentSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val content: String = if (rule != null) {
msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace)
@ -181,25 +188,25 @@ class WeworkAgentUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
val status = 0
SendUtils.updateLogs(logId, status, e.displayMessage)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, DingtalkResult::class.java)
if (resp?.errcode == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.errcode == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
}
fun sendMsg(setting: WeworkAgentSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}

@ -23,7 +23,9 @@ class WeworkRobotUtils private constructor() {
setting: WeworkRobotSetting,
msgInfo: MsgInfo,
rule: Rule?,
logId: Long?,
senderIndex: Int = 0,
logId: Long = 0L,
msgId: Long = 0L
) {
val content: String = if (rule != null) {
msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace)
@ -58,17 +60,16 @@ class WeworkRobotUtils private constructor() {
override fun onError(e: ApiException) {
Log.e(TAG, e.detailMessage)
SendUtils.updateLogs(logId, 0, e.displayMessage)
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
val resp = Gson().fromJson(response, WeworkRobotResult::class.java)
if (resp?.errcode == 0L) {
SendUtils.updateLogs(logId, 2, response)
} else {
SendUtils.updateLogs(logId, 0, response)
}
val status = if (resp?.errcode == 0L) 2 else 0
SendUtils.updateLogs(logId, status, response)
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
}
})
@ -76,7 +77,7 @@ class WeworkRobotUtils private constructor() {
}
fun sendMsg(setting: WeworkRobotSetting, msgInfo: MsgInfo) {
sendMsg(setting, msgInfo, null, null)
sendMsg(setting, msgInfo)
}
}
}

@ -0,0 +1,36 @@
package com.idormy.sms.forwarder.workers
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.Worker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SendLogicWorker(
context: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
val msgInfoJson = inputData.getString(Worker.sendMsgInfo)
val msgInfo = Gson().fromJson(msgInfoJson, MsgInfo::class.java)
val ruleId = inputData.getLong(Worker.ruleId, 0L)
val senderIndex = inputData.getInt(Worker.senderIndex, 0)
val msgId = inputData.getLong(Worker.msgId, 0L)
val rule = Core.rule.getOne(ruleId)
val sender = rule.senderList[senderIndex]
val log = Logs(0, rule.type, msgId, rule.id, sender.id)
val logId = Core.logs.insert(log)
SendUtils.sendMsgSender(msgInfo, rule, senderIndex, logId, msgId)
return@withContext Result.success()
}
}

@ -9,7 +9,8 @@ import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xutil.security.CipherUtils
@ -55,7 +56,6 @@ class SendWorker(
Log.e("SendWorker", "免打扰(禁用转发)时间段")
return@withContext Result.failure(workDataOf("send" to "failed"))
}
}
val msgInfoJson = inputData.getString(Worker.sendMsgInfo)
@ -75,18 +75,22 @@ class SendWorker(
//【注意】卡槽id-1=获取失败、0=卡槽1、1=卡槽2但是 Rule 表里存的是 SIM1/SIM2
val simSlot = "SIM" + (msgInfo.simSlot + 1)
val ruleList: List<RuleAndSender> = Core.rule.getRuleAndSender(msgInfo.type, 1, simSlot)
val ruleList: List<Rule> = Core.rule.getRuleList(msgInfo.type, 1, simSlot)
if (ruleList.isEmpty()) {
return@withContext Result.failure(workDataOf("send" to "failed"))
}
val msg = Msg(0, msgInfo.type, msgInfo.from, msgInfo.content, msgInfo.simSlot, msgInfo.simInfo, msgInfo.subId)
val msgId = Core.msg.insert(msg)
for (rule in ruleList) {
if (!rule.rule.checkMsg(msgInfo)) continue
val log = Logs(
0, msgInfo.type, msgInfo.from, msgInfo.content, rule.rule.id, msgInfo.simInfo, msgInfo.subId
)
Log.d("SendWorker", rule.toString())
if (!rule.checkMsg(msgInfo)) continue
val sender = rule.senderList[0]
val log = Logs(0, msgInfo.type, msgId, rule.id, sender.id)
val logId = Core.logs.insert(log)
SendUtils.sendMsgSender(msgInfo, rule.rule, rule.sender, logId)
SendUtils.sendMsgSender(msgInfo, rule, 0, logId, msgId)
}
} catch (e: Exception) {

@ -1,92 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<com.xuexiang.xui.widget.layout.XUIFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_view"
style="@style/XUILayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/config_margin_5dp"
android:layout_marginTop="@dimen/config_margin_5dp"
android:layout_marginEnd="@dimen/config_margin_5dp"
android:paddingStart="@dimen/config_padding_5dp"
android:paddingTop="@dimen/config_padding_5dp"
android:paddingEnd="@dimen/config_padding_5dp"
android:paddingBottom="@dimen/config_padding_5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_sender_image"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_status_image"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="24dp"
android:layout_marginTop="-16dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_from"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp" />
<ImageView
android:id="@+id/iv_sim_image"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="5dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:gravity="start"
android:maxEms="8"
android:maxLines="3"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<com.xuexiang.xui.widget.layout.XUIFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_view"
style="@style/XUILayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/config_margin_5dp"
android:layout_marginTop="@dimen/config_margin_5dp"
android:layout_marginEnd="@dimen/config_margin_5dp"
android:paddingStart="@dimen/config_padding_5dp"
android:paddingTop="@dimen/config_padding_5dp"
android:paddingEnd="@dimen/config_padding_5dp"
android:paddingBottom="@dimen/config_padding_5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_from"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp" />
<ImageView
android:id="@+id/iv_sim_image"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="5dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:gravity="start"
android:maxEms="8"
android:maxLines="3"
android:textSize="11sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="3dp"
android:layout_marginBottom="3dp"
android:background="?attr/xui_config_color_separator_light" />
<LinearLayout
android:id="@+id/layout_Logs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"></LinearLayout>
</LinearLayout>
</LinearLayout>
</com.xuexiang.xui.widget.layout.XUIFrameLayout>

@ -1,124 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<com.xuexiang.xui.widget.layout.XUIFrameLayout 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/card_view"
style="@style/XUILayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/config_margin_5dp"
android:layout_marginTop="@dimen/config_margin_5dp"
android:layout_marginEnd="@dimen/config_margin_5dp"
android:paddingStart="@dimen/config_padding_5dp"
android:paddingTop="@dimen/config_padding_5dp"
android:paddingEnd="@dimen/config_padding_5dp"
android:paddingBottom="@dimen/config_padding_5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_rule_image"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_rule_status"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="24dp"
android:layout_marginTop="-16dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/tv_rule_match"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="start"
android:maxEms="8"
android:maxLines="3"
android:textSize="11sp" />
<LinearLayout
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:layout_gravity="center_vertical"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
android:layout_width="24dp"
android:layout_height="24dp"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_sender_image"
android:layout_width="24dp"
android:layout_height="24dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_sender_status"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="14dp"
android:layout_marginTop="-10dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/tv_sender_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxEms="6"
android:maxLines="1"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<ImageView
android:id="@+id/iv_copy"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:layout_marginStart="10dp"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/colorStart"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_edit"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/toast_info_color"
tools:ignore="ContentDescription,PrivateResource" />
<ImageView
android:id="@+id/iv_delete"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/toast_error_color"
tools:ignore="ContentDescription,PrivateResource" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<com.xuexiang.xui.widget.layout.XUIFrameLayout 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/card_view"
style="@style/XUILayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/config_margin_5dp"
android:layout_marginTop="@dimen/config_margin_5dp"
android:layout_marginEnd="@dimen/config_margin_5dp"
android:paddingStart="@dimen/config_padding_5dp"
android:paddingTop="@dimen/config_padding_5dp"
android:paddingEnd="@dimen/config_padding_5dp"
android:paddingBottom="@dimen/config_padding_5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_rule_image"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_rule_status"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="24dp"
android:layout_marginTop="-16dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_rule_match"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="5dp"
android:ellipsize="end"
android:gravity="start"
android:maxEms="8"
android:maxLines="3"
android:textSize="11sp" />
<LinearLayout
android:id="@+id/layout_Senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:orientation="horizontal"></LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/iv_copy"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:layout_marginStart="10dp"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/colorStart"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_edit"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/toast_info_color"
tools:ignore="ContentDescription,PrivateResource" />
<ImageView
android:id="@+id/iv_delete"
android:layout_width="@dimen/card_view_image_size"
android:layout_height="@dimen/card_view_image_size"
android:padding="@dimen/card_view_image_padding"
app:tint="@color/toast_error_color"
tools:ignore="ContentDescription,PrivateResource" />
</LinearLayout>
</com.xuexiang.xui.widget.layout.XUIFrameLayout>

File diff suppressed because it is too large Load Diff

@ -1,262 +1,277 @@
<?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/senderBarStyleWithSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sender_name_status"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:singleLine="true"
app:met_clearButton="true" />
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_enable"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Method"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_method_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/post" />
<RadioButton
android:id="@+id/rb_method_get"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/get" />
<RadioButton
android:id="@+id/rb_method_put"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/put" />
<RadioButton
android:id="@+id/rb_method_patch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/patch" />
</RadioGroup>
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
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:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/webhook_server"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/webhook_server_tips"
android:textSize="10sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_webServer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text='@string/webhook_params'
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/webhook_params_tips"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_webParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional"
android:inputType="textUri"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/webhook_secret"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_Secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional"
android:singleLine="true"
app:met_passWordButton="true" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_Headers"
style="@style/senderBarStyle"
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/headers"
android:textStyle="bold" />
<com.xuexiang.xui.widget.button.shadowbutton.ShadowButton
android:id="@+id/btn_add_header"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="10dp"
android:background="@drawable/icon_add"
app:sb_shape_type="round" />
</LinearLayout>
</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" />
<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" />
<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" />
</LinearLayout>
<?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/senderBarStyleWithSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sender_name_status"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:singleLine="true"
app:met_clearButton="true" />
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_enable"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Method"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_method_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:scaleX="0.7"
android:scaleY="0.7"
android:text="@string/post"
android:textSize="14sp"
android:translationX="18dp" />
<RadioButton
android:id="@+id/rb_method_get"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0.7"
android:scaleY="0.7"
android:text="@string/get"
android:textSize="14sp"
android:translationX="18dp" />
<RadioButton
android:id="@+id/rb_method_put"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0.7"
android:scaleY="0.7"
android:text="@string/put"
android:textSize="14sp"
android:translationX="18dp" />
<RadioButton
android:id="@+id/rb_method_patch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="0.7"
android:scaleY="0.7"
android:text="@string/patch"
android:textSize="14sp"
android:translationX="18dp" />
</RadioGroup>
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
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:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/webhook_server"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/webhook_server_tips"
android:textSize="10sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_webServer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text='@string/webhook_params'
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/webhook_params_tips"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_webParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional"
android:inputType="textUri"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/webhook_secret"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_Secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional"
android:singleLine="true"
app:met_passWordButton="true" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_Headers"
style="@style/senderBarStyle"
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/headers"
android:textStyle="bold" />
<com.xuexiang.xui.widget.button.shadowbutton.ShadowButton
android:id="@+id/btn_add_header"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="10dp"
android:background="@drawable/icon_add"
app:sb_shape_type="round" />
</LinearLayout>
</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" />
<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" />
<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" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,70 @@
<?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="@color/xui_config_color_white"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingEnd="5dp"
tools:ignore="UseCompoundDrawables">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="24dp"
android:layout_height="24dp"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_sender_image"
android:layout_width="24dp"
android:layout_height="24dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_sender_status"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="14dp"
android:layout_marginTop="-10dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/tv_sender_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="marquee"
android:minHeight="?attr/ms_item_height_size"
android:paddingStart="?attr/ms_padding_left_size"
android:paddingTop="?attr/ms_padding_top_size"
android:paddingEnd="?attr/ms_padding_left_size"
android:paddingBottom="?attr/ms_padding_top_size"
android:singleLine="true"
tools:ignore="PrivateResource" />
<ImageView
android:id="@+id/iv_remove_sender"
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@string/sender_del"
android:src="@drawable/icon_delete"
app:tint="@color/design_default_color_error" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/xui_config_color_separator_light" />
</LinearLayout>

@ -0,0 +1,44 @@
<?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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_sender_image"
android:layout_width="24dp"
android:layout_height="24dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_sender_status"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="14dp"
android:layout_marginTop="-10dp"
tools:ignore="ContentDescription,VisualLintBounds" />
</LinearLayout>
<TextView
android:id="@+id/tv_sender_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxEms="10"
android:maxLines="1"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>

@ -0,0 +1,44 @@
<?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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_sender_image"
android:layout_width="24dp"
android:layout_height="24dp"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/iv_sender_status"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="14dp"
android:layout_marginTop="-10dp"
tools:ignore="ContentDescription,VisualLintBounds" />
</LinearLayout>
<TextView
android:id="@+id/tv_sender_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxEms="10"
android:maxLines="1"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>

@ -223,6 +223,10 @@
<string name="test_package_name">测试模拟的APP包名</string>
<string name="test_inform_title">测试模拟的通知标题</string>
<string name="test_inform_content">测试模拟的通知内容</string>
<string name="sender_logic">执行逻辑</string>
<string name="sender_logic_all">全部执行</string>
<string name="sender_logic_until_fail">失败即止</string>
<string name="sender_logic_until_success">成功即止</string>
<string name="match_sim_slot">匹配卡槽</string>
<string name="match_field">匹配字段</string>
<string name="phone_number">手机号</string>
@ -841,6 +845,7 @@
<string name="frpc_failed_to_run">Frpc运行失败</string>
<string name="successfully_deleted">删除成功</string>
<string name="sender_disabled_tips">【注意】该发送通道已经禁用,其关联的规则即便匹配上也不会发送!</string>
<string name="sender_contains_tips">【注意】该发送通道已经在列表中,无需重复添加!</string>
<string name="local_call">本地呼叫:</string>
<string name="remote_sms">远程发短信:</string>
<string name="clear">清除</string>
@ -942,4 +947,6 @@
<string name="copy_public_key">复制公钥</string>
<string name="sm4_key">SM4密钥</string>
<string name="sm4_key_tips">客户端/服务端交互采用SM4加解密</string>
<string name="sender_del">删除发送通道</string>
</resources>

Loading…
Cancel
Save