新增:Socket发送通道(支持MQTT/TCP/UDP协议) #252

pull/286/head
pppscn 1 year ago
parent 419766b47a
commit 565795a843

@ -188,6 +188,9 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//frpc
implementation files('libs/frpclib.aar')
//kmnktKotlin MultiplatformsocketAndroidJVMUDP/TCP/MQTT使
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
implementation files('libs/socket.aar')
testImplementation deps.junit
androidTestImplementation 'androidx.test.ext:junit:1.1.5'

Binary file not shown.

@ -282,6 +282,8 @@
-keep class javax.activation.** { *;}
-keep class com.smailnet.emailkit.** { *;}
-keep class com.idormy.sms.forwarder.utils.mail.** {*;}
-keep class com.gitee.xuankaicat.kmnkt.** {*;}
-keep class org.eclipse.paho.client.** {*;}
-keep public class com.xuexiang.xrouter.routes.**{*;}
-keep class * implements com.xuexiang.xrouter.facade.template.ISyringe{*;}

@ -54,6 +54,7 @@ data class LogsDetail(
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
TYPE_SOCKET -> R.drawable.icon_socket
else -> R.drawable.icon_sms
}
}

@ -38,6 +38,7 @@ data class Sender(
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
TYPE_SOCKET -> R.drawable.icon_socket
else -> R.drawable.icon_sms
}
@ -60,6 +61,7 @@ data class Sender(
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
TYPE_SOCKET -> R.drawable.icon_socket
else -> R.drawable.icon_sms
}

@ -10,7 +10,7 @@ class ConvertersSenderList {
@TypeConverter
fun stringToObject(value: String): List<Sender> {
var senderList: MutableList<Sender> = mutableListOf()
val senderList: MutableList<Sender> = mutableListOf()
value.split(",").map { it.trim() }.forEach {
val sender = Core.sender.getOne(it.toLong())
senderList.add(sender)
@ -20,7 +20,7 @@ class ConvertersSenderList {
@TypeConverter
fun objectToString(list: List<Sender>): String {
var senderList = ArrayList<Long>()
val senderList = ArrayList<Long>()
list.forEach {
senderList += it.id
}

@ -0,0 +1,32 @@
package com.idormy.sms.forwarder.entity.setting
import com.idormy.sms.forwarder.R
import java.io.Serializable
data class SocketSetting(
val method: String? = "MQTT",
var address: String = "", //IP地址
val port: Int = 0, //端口号
val msgTemplate: String = "", //消息模板
val secret: String? = "", //签名密钥
val username: String = "", //用户名
val password: String = "", //密码
val inCharset: String = "", //输入编码
val outCharset: String = "", //输出编码
val inMessageTopic: String = "", //Mqtt专属输入信息响应主题即接收对应主题的消息
val outMessageTopic: String = "", //Mqtt专属输出信息响应主题即发送对应主题的消息
val uriType: String = "tcp", //Mqtt专属通信方式 默认为tcp
val path: String = "", //Mqtt专属通信路径用于在使用ws进行通信时设置uri最后的访问结果为"${uriType}://${address}:${port}${path}"
val clientId: String = "", //Mqtt专属客户端ID如果为空则为随机值
) : Serializable {
fun getMethodCheckId(): Int {
return when (method) {
null, "MQTT" -> R.id.rb_method_mqtt
"TCP" -> R.id.rb_method_tcp
"UDP" -> R.id.rb_method_udp
else -> R.id.rb_method_mqtt
}
}
}

@ -110,6 +110,7 @@ class SendersFragment : BaseFragment<FragmentSendersBinding?>(), SenderPagingAda
TYPE_DINGTALK_INNER_ROBOT -> DingtalkInnerRobotFragment::class.java
TYPE_FEISHU_APP -> FeishuAppFragment::class.java
TYPE_URL_SCHEME -> UrlSchemeFragment::class.java
TYPE_SOCKET -> SocketFragment::class.java
else -> DingtalkGroupRobotFragment::class.java
}
).setNewActivity(true)
@ -136,6 +137,7 @@ class SendersFragment : BaseFragment<FragmentSendersBinding?>(), SenderPagingAda
TYPE_DINGTALK_INNER_ROBOT -> DingtalkInnerRobotFragment::class.java
TYPE_FEISHU_APP -> FeishuAppFragment::class.java
TYPE_URL_SCHEME -> UrlSchemeFragment::class.java
TYPE_SOCKET -> SocketFragment::class.java
else -> DingtalkGroupRobotFragment::class.java
}
).setNewActivity(true)

@ -0,0 +1,252 @@
package com.idormy.sms.forwarder.fragment.senders
import android.os.Looper
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioGroup
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.SenderViewModel
import com.idormy.sms.forwarder.databinding.FragmentSendersSocketBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.SocketSetting
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.sender.SocketUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.util.*
@Page(name = "Socket")
@Suppress("PrivatePropertyName")
class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnClickListener {
private val TAG: String = SocketFragment::class.java.simpleName
var titleBar: TitleBar? = null
private val viewModel by viewModels<SenderViewModel> { BaseViewModelFactory(context) }
private var mCountDownHelper: CountDownButtonHelper? = null
@JvmField
@AutoWired(name = KEY_SENDER_ID)
var senderId: Long = 0
@JvmField
@AutoWired(name = KEY_SENDER_TYPE)
var senderType: Int = 0
@JvmField
@AutoWired(name = KEY_SENDER_CLONE)
var isClone: Boolean = false
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentSendersSocketBinding {
return FragmentSendersSocketBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.socket)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, SettingUtils.requestTimeout)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnTest.text = getString(R.string.test)
}
})
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
binding!!.btnDel.setText(R.string.discard)
return
}
//编辑
binding!!.btnDel.setText(R.string.del)
AppDatabase.getInstance(requireContext()).senderDao().get(senderId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver<Sender> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onSuccess(sender: Sender) {
if (isClone) {
titleBar?.setSubTitle(getString(R.string.clone_sender) + ": " + sender.name)
binding!!.btnDel.setText(R.string.discard)
} else {
titleBar?.setSubTitle(getString(R.string.edit_sender) + ": " + sender.name)
}
binding!!.etName.setText(sender.name)
binding!!.sbEnable.isChecked = sender.status == 1
val settingVo = Gson().fromJson(sender.jsonSetting, SocketSetting::class.java)
Log.d(TAG, settingVo.toString())
if (settingVo != null) {
val checkedId = settingVo.getMethodCheckId()
binding!!.rgMethod.check(checkedId)
binding!!.etAddress.setText(settingVo.address)
binding!!.etPort.setText(settingVo.port.toString())
binding!!.etMsgTemplate.setText(settingVo.msgTemplate)
binding!!.etSecret.setText(settingVo.secret)
binding!!.etUsername.setText(settingVo.username)
binding!!.etPassword.setText(settingVo.password)
binding!!.etInCharset.setSelectedItem(settingVo.inCharset)
binding!!.etOutCharset.setSelectedItem(settingVo.outCharset)
binding!!.etInMessageTopic.setText(settingVo.inMessageTopic)
binding!!.etOutMessageTopic.setText(settingVo.outMessageTopic)
binding!!.etUriType.setText(settingVo.uriType)
binding!!.etPath.setText(settingVo.path)
binding!!.etClientId.setText(settingVo.clientId)
binding!!.layoutMqtt.visibility = if (checkedId == R.id.rb_method_mqtt) View.VISIBLE else View.GONE
}
}
})
}
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.rgMethod.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
binding!!.layoutMqtt.visibility = if (checkedId == R.id.rb_method_mqtt) View.VISIBLE else View.GONE
}
LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() }
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_test -> {
mCountDownHelper?.start()
Thread {
try {
val settingVo = checkSetting()
Log.d(TAG, settingVo.toString())
val msgInfo = MsgInfo(
"sms", getString(R.string.test_phone_num), getString(R.string.test_sender_sms), Date(), getString(R.string.test_sim_info)
)
SocketUtils.sendMsg(settingVo, msgInfo)
} catch (e: Exception) {
e.printStackTrace()
if (Looper.myLooper() == null) Looper.prepare()
XToastUtils.error(e.message.toString())
Looper.loop()
}
LiveEventBus.get(KEY_SENDER_TEST, String::class.java).post("finish")
}.start()
return
}
R.id.btn_del -> {
if (senderId <= 0 || isClone) {
popToBack()
return
}
MaterialDialog.Builder(requireContext()).title(R.string.delete_sender_title).content(R.string.delete_sender_tips).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(senderId)
XToastUtils.success(R.string.delete_sender_toast)
popToBack()
}.show()
return
}
R.id.btn_save -> {
val name = binding!!.etName.text.toString().trim()
if (TextUtils.isEmpty(name)) {
throw Exception(getString(R.string.invalid_name))
}
val status = if (binding!!.sbEnable.isChecked) 1 else 0
val settingVo = checkSetting()
if (isClone) senderId = 0
val senderNew = Sender(senderId, senderType, name, Gson().toJson(settingVo), status)
Log.d(TAG, senderNew.toString())
viewModel.insertOrUpdate(senderNew)
XToastUtils.success(R.string.tipSaveSuccess)
popToBack()
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
e.printStackTrace()
}
}
private fun checkSetting(): SocketSetting {
val address = binding!!.etAddress.text.toString().trim()
if (CommonUtils.checkIP(address) == "Neither" && !CommonUtils.checkDomain(address)) {
throw Exception(getString(R.string.invalid_ip))
}
val port = binding!!.etPort.text.toString().trim()
if (!CommonUtils.checkPort(port)) {
throw Exception(getString(R.string.invalid_port))
}
val method = when (binding!!.rgMethod.checkedRadioButtonId) {
R.id.rb_method_tcp -> "TCP"
R.id.rb_method_udp -> "UDP"
else -> "MQTT"
}
val msgTemplate = binding!!.etMsgTemplate.text.toString().trim()
val secret = binding!!.etSecret.text.toString().trim()
val username = binding!!.etUsername.text.toString().trim()
val password = binding!!.etPassword.text.toString().trim()
val inCharset = binding!!.etInCharset.text.toString().trim()
val outCharset = binding!!.etOutCharset.text.toString().trim()
val inMessageTopic = binding!!.etInMessageTopic.text.toString().trim()
val outMessageTopic = binding!!.etOutMessageTopic.text.toString().trim()
val uriType = binding!!.etUriType.text.toString().trim()
val path = binding!!.etPath.text.toString().trim()
val clientId = binding!!.etClientId.text.toString().trim()
if (method == "MQTT" && (TextUtils.isEmpty(inMessageTopic) || TextUtils.isEmpty(outMessageTopic))) {
throw Exception(getString(R.string.invalid_mqtt_message_topic))
}
return SocketSetting(method, address, port.toInt(), msgTemplate, secret, username, password, inCharset, outCharset, inMessageTopic, outMessageTopic, uriType, path, clientId)
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
}

@ -60,49 +60,34 @@ class CommonUtils private constructor() {
@Suppress("SameParameterValue", "NAME_SHADOWING")
@JvmStatic
fun showPrivacyDialog(context: Context, submitListener: SingleButtonCallback?): Dialog {
val dialog =
MaterialDialog.Builder(context).title(R.string.title_reminder).autoDismiss(false)
.cancelable(false)
.positiveText(R.string.lab_agree)
.onPositive { dialog1: MaterialDialog, which: DialogAction? ->
if (submitListener != null) {
submitListener.onClick(dialog1, which!!)
} else {
dialog1.dismiss()
}
}
.negativeText(R.string.lab_disagree).onNegative { dialog, _ ->
val dialog = MaterialDialog.Builder(context).title(R.string.title_reminder).autoDismiss(false).cancelable(false).positiveText(R.string.lab_agree).onPositive { dialog1: MaterialDialog, which: DialogAction? ->
if (submitListener != null) {
submitListener.onClick(dialog1, which!!)
} else {
dialog1.dismiss()
}
}.negativeText(R.string.lab_disagree).onNegative { dialog, _ ->
dialog.dismiss()
DialogLoader.getInstance().showConfirmDialog(
context, ResUtils.getString(R.string.title_reminder), String.format(
ResUtils.getString(R.string.content_privacy_explain_again), ResUtils.getString(R.string.app_name)
), ResUtils.getString(R.string.lab_look_again), { dialog, _ ->
dialog.dismiss()
DialogLoader.getInstance().showConfirmDialog(
context,
ResUtils.getString(R.string.title_reminder),
String.format(
ResUtils.getString(R.string.content_privacy_explain_again),
ResUtils.getString(R.string.app_name)
),
ResUtils.getString(R.string.lab_look_again),
{ dialog, _ ->
dialog.dismiss()
showPrivacyDialog(context, submitListener)
},
ResUtils.getString(R.string.lab_still_disagree)
) { dialog, _ ->
showPrivacyDialog(context, submitListener)
}, ResUtils.getString(R.string.lab_still_disagree)
) { dialog, _ ->
dialog.dismiss()
DialogLoader.getInstance().showConfirmDialog(
context, ResUtils.getString(R.string.content_think_about_it_again), ResUtils.getString(R.string.lab_look_again), { dialog, _ ->
dialog.dismiss()
DialogLoader.getInstance().showConfirmDialog(
context,
ResUtils.getString(R.string.content_think_about_it_again),
ResUtils.getString(R.string.lab_look_again),
{ dialog, _ ->
dialog.dismiss()
showPrivacyDialog(context, submitListener)
},
ResUtils.getString(R.string.lab_exit_app)
) { dialog, _ ->
dialog.dismiss()
XUtil.exitApp()
}
}
}.build()
showPrivacyDialog(context, submitListener)
}, ResUtils.getString(R.string.lab_exit_app)
) { dialog, _ ->
dialog.dismiss()
XUtil.exitApp()
}
}
}.build()
dialog.setContent(getPrivacyContent(context))
//开始响应点击事件
dialog.contentView!!.movementMethod = LinkMovementMethod.getInstance()
@ -114,15 +99,7 @@ class CommonUtils private constructor() {
* @return 隐私政策说明
*/
private fun getPrivacyContent(context: Context): SpannableStringBuilder {
return SpannableStringBuilder()
.append(" ").append(ResUtils.getString(R.string.privacy_content_1)).append(" ").append(ResUtils.getString(R.string.app_name)).append("!\n")
.append(" ").append(ResUtils.getString(R.string.privacy_content_2))
.append(" ").append(ResUtils.getString(R.string.privacy_content_3))
.append(getPrivacyLink(context, PRIVACY_URL))
.append(ResUtils.getString(R.string.privacy_content_4))
.append(" ").append(ResUtils.getString(R.string.privacy_content_5))
.append(getPrivacyLink(context, PRIVACY_URL))
.append(ResUtils.getString(R.string.privacy_content_6))
return SpannableStringBuilder().append(" ").append(ResUtils.getString(R.string.privacy_content_1)).append(" ").append(ResUtils.getString(R.string.app_name)).append("!\n").append(" ").append(ResUtils.getString(R.string.privacy_content_2)).append(" ").append(ResUtils.getString(R.string.privacy_content_3)).append(getPrivacyLink(context, PRIVACY_URL)).append(ResUtils.getString(R.string.privacy_content_4)).append(" ").append(ResUtils.getString(R.string.privacy_content_5)).append(getPrivacyLink(context, PRIVACY_URL)).append(ResUtils.getString(R.string.privacy_content_6))
}
/**
@ -132,8 +109,7 @@ class CommonUtils private constructor() {
@Suppress("SameParameterValue")
private fun getPrivacyLink(context: Context, privacyUrl: String): SpannableString {
val privacyName = String.format(
ResUtils.getString(R.string.lab_privacy_name),
ResUtils.getString(R.string.app_name)
ResUtils.getString(R.string.lab_privacy_name), ResUtils.getString(R.string.app_name)
)
val spannableString = SpannableString(privacyName)
spannableString.setSpan(object : ClickableSpan() {
@ -165,15 +141,11 @@ class CommonUtils private constructor() {
*/
@JvmStatic
fun gotoProtocol(fragment: XPageFragment?, isPrivacy: Boolean, isImmersive: Boolean) {
PageOption.to(ServiceProtocolFragment::class.java)
.putString(
ServiceProtocolFragment.KEY_PROTOCOL_TITLE,
if (isPrivacy) ResUtils.getString(R.string.title_privacy_protocol) else ResUtils.getString(
R.string.title_user_protocol
)
PageOption.to(ServiceProtocolFragment::class.java).putString(
ServiceProtocolFragment.KEY_PROTOCOL_TITLE, if (isPrivacy) ResUtils.getString(R.string.title_privacy_protocol) else ResUtils.getString(
R.string.title_user_protocol
)
.putBoolean(ServiceProtocolFragment.KEY_IS_IMMERSIVE, isImmersive)
.open(fragment!!)
).putBoolean(ServiceProtocolFragment.KEY_IS_IMMERSIVE, isImmersive).open(fragment!!)
}
/**
@ -210,13 +182,7 @@ class CommonUtils private constructor() {
}
val bounds = Rect()
view?.getGlobalVisibleRect(bounds)
PreviewBuilder.from(fragment)
.setImgs(ImageInfo.newInstance(url, bounds))
.setCurrentIndex(0)
.setSingleFling(true)
.setProgressColor(R.color.xui_config_color_main_theme)
.setType(PreviewBuilder.IndicatorType.Number)
.start()
PreviewBuilder.from(fragment).setImgs(ImageInfo.newInstance(url, bounds)).setCurrentIndex(0).setSingleFling(true).setProgressColor(R.color.xui_config_color_main_theme).setType(PreviewBuilder.IndicatorType.Number).start()
}
/**
@ -228,11 +194,7 @@ class CommonUtils private constructor() {
*/
@JvmStatic
fun previewMarkdown(fragment: XPageFragment?, title: String, url: String, isImmersive: Boolean) {
PageOption.to(MarkdownFragment::class.java)
.putString(MarkdownFragment.KEY_MD_TITLE, title)
.putString(MarkdownFragment.KEY_MD_URL, url)
.putBoolean(MarkdownFragment.KEY_IS_IMMERSIVE, isImmersive)
.open(fragment!!)
PageOption.to(MarkdownFragment::class.java).putString(MarkdownFragment.KEY_MD_TITLE, title).putString(MarkdownFragment.KEY_MD_URL, url).putBoolean(MarkdownFragment.KEY_IS_IMMERSIVE, isImmersive).open(fragment!!)
}
//是否合法的url
@ -243,7 +205,7 @@ class CommonUtils private constructor() {
//是否合法的url
fun checkUrl(urls: String?, emptyResult: Boolean): Boolean {
if (TextUtils.isEmpty(urls)) return emptyResult
val regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\\[\\]]*[-a-zA-Z0-9+&@#/%=~_|\\[\\]]"
val regex = "^(https?|ftp|file)://[-a-zA-Z\\d+&@#/%?=~_|!:,.;\\[\\]]*[-a-zA-Z\\d+&@#/%=~_|\\[\\]]"
val pat = Pattern.compile(regex)
val mat = pat.matcher(urls?.trim() ?: "")
return mat.matches()
@ -257,12 +219,41 @@ class CommonUtils private constructor() {
//是否合法的URL Scheme
fun checkUrlScheme(urls: String?, emptyResult: Boolean): Boolean {
if (TextUtils.isEmpty(urls)) return emptyResult
val regex = "^[a-zA-Z0-9]+://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\\[\\]]*[-a-zA-Z0-9+&@#/%=~_|\\[\\]]"
val regex = "^[a-zA-Z\\d]+://[-a-zA-Z\\d+&@#/%?=~_|!:,.;\\[\\]]*[-a-zA-Z\\d+&@#/%=~_|\\[\\]]"
val pat = Pattern.compile(regex)
val mat = pat.matcher(urls?.trim() ?: "")
return mat.matches()
}
//是否合法的IP地址
fun checkIP(IP: String): String {
if (TextUtils.isEmpty(IP)) return "Neither"
if (IP.contains(".")) {
val chunkIPv4 = "([\\d]|[1-9][\\d]|1[\\d][\\d]|2[0-4][\\d]|25[0-5])"
val pattenIPv4 = Pattern.compile("^($chunkIPv4\\.){3}$chunkIPv4$")
return if (pattenIPv4.matcher(IP).matches()) "IPv4" else "Neither"
} else if (IP.contains(":")) {
val chunkIPv6 = "([\\da-fA-F]{1,4})"
val pattenIPv6 = Pattern.compile("^($chunkIPv6\\:){7}$chunkIPv6$")
return if (pattenIPv6.matcher(IP).matches()) "IPv6" else "Neither"
}
return "Neither"
}
//是否合法的域名
fun checkDomain(domain: String): Boolean {
val pattenDomain = Pattern.compile("^(?=^.{3,255}$)(?:(?:(?:[a-zA-Z\\d]|[a-zA-Z\\d][a-zA-Z\\d\\-]*[a-zA-Z\\d])\\.){1,126}(?:[A-Za-z\\d]|[A-Za-z\\d][A-Za-z\\d\\-]*[A-Za-z\\d]))$")
return pattenDomain.matcher(domain).matches()
}
//是否合法的端口号
fun checkPort(port: String): Boolean {
if (TextUtils.isEmpty(port)) return false
val pattenPort = Pattern.compile("^((6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])|[0-5]?\\d{0,4})$")
return pattenPort.matcher(port).matches()
}
//是否启用通知监听服务
fun isNotificationListenerServiceEnabled(context: Context): Boolean {
val packageNames = NotificationManagerCompat.getEnabledListenerPackages(context)
@ -273,12 +264,10 @@ class CommonUtils private constructor() {
fun toggleNotificationListenerService(context: Context) {
val pm = context.packageManager
pm.setComponentEnabledSetting(
ComponentName(context.applicationContext, NotifyService::class.java),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
ComponentName(context.applicationContext, NotifyService::class.java), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
ComponentName(context.applicationContext, NotifyService::class.java),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
ComponentName(context.applicationContext, NotifyService::class.java), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
}

@ -179,6 +179,7 @@ const val TYPE_GOTIFY = 11
const val TYPE_DINGTALK_INNER_ROBOT = 12
const val TYPE_FEISHU_APP = 13
const val TYPE_URL_SCHEME = 14
const val TYPE_SOCKET = 15
var SENDER_FRAGMENT_LIST = listOf(
PageInfo(
getString(R.string.dingtalk_robot),
@ -285,6 +286,13 @@ var SENDER_FRAGMENT_LIST = listOf(
CoreAnim.slide,
R.drawable.icon_url_scheme
),
PageInfo(
getString(R.string.socket),
"com.idormy.sms.forwarder.fragment.senders.SocketFragment",
"{\"\":\"\"}",
CoreAnim.slide,
R.drawable.icon_socket
),
)
//前台服务

@ -54,7 +54,7 @@ object SendUtils {
}
}
var rule = item.rule
val rule = item.rule
rule.senderLogic = SENDER_LOGIC_RETRY
sendMsgSender(msgInfo, rule, senderIndex, logId, item.msg.id)
}
@ -124,6 +124,10 @@ object SendUtils {
val settingVo = Gson().fromJson(sender.jsonSetting, UrlSchemeSetting::class.java)
UrlSchemeUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
TYPE_SOCKET -> {
val settingVo = Gson().fromJson(sender.jsonSetting, SocketSetting::class.java)
SocketUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId)
}
else -> {
updateLogs(logId, 0, "未知发送通道")
}

@ -0,0 +1,182 @@
package com.idormy.sms.forwarder.utils.sender
import android.annotation.SuppressLint
import android.text.TextUtils
import android.util.Base64
import com.gitee.xuankaicat.kmnkt.socket.MqttQuality
import com.gitee.xuankaicat.kmnkt.socket.dsl.mqtt
import com.gitee.xuankaicat.kmnkt.socket.dsl.tcp
import com.gitee.xuankaicat.kmnkt.socket.dsl.udp
import com.gitee.xuankaicat.kmnkt.socket.open
import com.gitee.xuankaicat.kmnkt.socket.utils.Charset
import com.google.gson.Gson
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.SocketSetting
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.xuexiang.xutil.app.AppUtils
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@Suppress("PrivatePropertyName", "UNUSED_PARAMETER", "unused")
class SocketUtils {
companion object {
private val TAG: String = SocketUtils::class.java.simpleName
fun sendMsg(
setting: SocketSetting, msgInfo: MsgInfo, rule: Rule? = null, senderIndex: Int = 0, logId: Long = 0L, msgId: Long = 0L
) {
val from: String = msgInfo.from
val content: String = if (rule != null) {
msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace)
} else {
msgInfo.getContentForSend(SettingUtils.smsTemplate)
}
val timestamp = System.currentTimeMillis()
val orgContent: String = msgInfo.content
val deviceMark: String = SettingUtils.extraDeviceMark
val appVersion: String = AppUtils.getAppVersionName()
val simInfo: String = msgInfo.simInfo
@SuppressLint("SimpleDateFormat") val receiveTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()) //smsVo.getDate()
var sign = ""
if (!TextUtils.isEmpty(setting.secret)) {
val stringToSign = "$timestamp\n" + setting.secret
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(setting.secret?.toByteArray(StandardCharsets.UTF_8), "HmacSHA256"))
val signData = mac.doFinal(stringToSign.toByteArray(StandardCharsets.UTF_8))
sign = URLEncoder.encode(String(Base64.encode(signData, Base64.NO_WRAP)), "UTF-8")
}
var message = if (TextUtils.isEmpty(setting.msgTemplate)) "{\"msg\": \"[msg]\"}" else setting.msgTemplate
message = if (message.startsWith("{")) {
message.replace("[from]", from).replace("[content]", escapeJson(content)).replace("[msg]", escapeJson(content)).replace("[org_content]", escapeJson(orgContent)).replace("[device_mark]", escapeJson(deviceMark)).replace("[app_version]", appVersion).replace("[title]", escapeJson(simInfo)).replace("[card_slot]", escapeJson(simInfo)).replace("[receive_time]", receiveTime).replace("[timestamp]", timestamp.toString()).replace("[sign]", sign)
} else {
message.replace("[from]", URLEncoder.encode(from, "UTF-8")).replace("[content]", URLEncoder.encode(content, "UTF-8")).replace("[msg]", URLEncoder.encode(content, "UTF-8")).replace("[org_content]", URLEncoder.encode(orgContent, "UTF-8")).replace("[device_mark]", URLEncoder.encode(deviceMark, "UTF-8")).replace("[app_version]", URLEncoder.encode(appVersion, "UTF-8")).replace("[title]", URLEncoder.encode(simInfo, "UTF-8")).replace("[card_slot]", URLEncoder.encode(simInfo, "UTF-8")).replace("[receive_time]", URLEncoder.encode(receiveTime, "UTF-8")).replace("\n", "%0A").replace("[timestamp]", timestamp.toString()).replace("[sign]", sign)
}
if (setting.method == "TCP" || setting.method == "UDP") {
var isReceived = false
var isConnected = false
val socket = if (setting.method == "TCP") {
tcp {
address = setting.address//设置ip地址
port = setting.port//设置端口号
if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码
if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码
}
} else {
udp {
address = setting.address//设置ip地址
port = setting.port//设置端口号
if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码
if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码
}
}
socket.open {
success {
//开启连接成功时执行
isConnected = true
SendUtils.updateLogs(logId, 1, "TCP连接成功")
socket.send(message)
socket.startReceive { str, data ->
isReceived = true
android.util.Log.d(TAG, "str=$str,data=$data")
SendUtils.updateLogs(logId, 2, "收到订阅消息str=$str,data=$data")
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
return@startReceive false
}
}
failure {
//开启连接失败时执行
val status = 0
SendUtils.updateLogs(logId, status, "TCP连接失败")
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
return@failure false//是否继续尝试连接
}
loss {
//失去连接时执行
return@loss false//是否尝试重连
}
}
//延时5秒关闭连接
if (isConnected) {
Thread.sleep(5000)
socket.stopReceive()
socket.close()
if (!isReceived) {
SendUtils.updateLogs(logId, 0, "未收到订阅消息")
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
}
}
return
} else if (setting.method == "MQTT") {
val mqtt = mqtt {
address = setting.address//设置ip地址
port = setting.port//设置端口号
if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码
if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码
if (!TextUtils.isEmpty(setting.username)) username = setting.username
if (!TextUtils.isEmpty(setting.password)) password = setting.password
if (!TextUtils.isEmpty(setting.inMessageTopic)) inMessageTopic = setting.inMessageTopic
if (!TextUtils.isEmpty(setting.outMessageTopic)) outMessageTopic = setting.outMessageTopic
//自定义配置
qos = MqttQuality.ExactlyOnce // 服务质量 详见MqttQuality
if (!TextUtils.isEmpty(setting.uriType)) uriType = setting.uriType //通信方式 默认为tcp
if (!TextUtils.isEmpty(setting.clientId)) clientId = setting.clientId //客户端ID如果为空则为随机值
timeOut = 10 //设置超时时间
cleanSession = true //断开连接后是否清楚缓存,如果清除缓存则在重连后需要手动恢复订阅。
keepAliveInterval = 20 //检测连接是否中断的间隔
//行为配置
threadLock = false //是否启用线程同步锁 默认false
}
mqtt.open {
success {
//开启连接成功时执行
SendUtils.updateLogs(logId, 1, "MQTT连接成功")
// 订阅并发布后等待至拿到响应消息并赋值给result
// 如果超过10秒没有收到消息则将result设为"消息响应超时"并取消订阅topic
val response = mqtt.sendAndReceiveSync(setting.outMessageTopic, setting.inMessageTopic, message, 10000L) ?: "消息响应超时"
mqtt.close()
val status = if (response == "消息响应超时") 0 else 2
SendUtils.updateLogs(logId, status, "收到订阅消息:$response")
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
return@success
}
failure {
//开启连接失败时执行
val status = 0
SendUtils.updateLogs(logId, status, "MQTT连接失败")
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
return@failure false//是否继续尝试连接
}
loss {
//失去连接时执行
return@loss false//是否尝试重连
}
}
}
}
//JSON需要转义的字符
private fun escapeJson(str: String?): String {
if (str == null) return "null"
val jsonStr: String = Gson().toJson(str)
return if (jsonStr.length >= 2) jsonStr.substring(1, jsonStr.length - 1) else jsonStr
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -16,70 +16,62 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="vertical"
tools:ignore="DisableBaselineAlignment">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
<TextView
android:id="@+id/tv_from"
android:layout_width="0dp"
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>
android:layout_weight="1" />
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:id="@+id/tv_time"
android:layout_width="wrap_content"
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" />
android:layout_marginStart="5dp" />
<LinearLayout
android:id="@+id/layout_Logs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"></LinearLayout>
<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>
</com.xuexiang.xui.widget.layout.XUIFrameLayout>

@ -65,7 +65,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:orientation="horizontal"></LinearLayout>
android:orientation="horizontal" />
</LinearLayout>

@ -59,7 +59,7 @@
android:id="@+id/layout_Senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"></LinearLayout>
android:orientation="vertical" />
<LinearLayout
android:id="@+id/layout_sender_logic"

@ -0,0 +1,440 @@
<?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="5dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_method_mqtt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/mqtt"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rb_method_tcp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tcp"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rb_method_udp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/udp"
android:textSize="14sp" />
</RadioGroup>
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_ip"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="3"
android:singleLine="true"
app:met_clearButton="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/server_port"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:digits="0123456789"
android:inputType="number"
android:maxLength="5"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/username"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/optional"
android:singleLine="true"
app:met_clearButton="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/password"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/optional"
android:singleLine="true"
app:met_passWordButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/in_charset"
android:textStyle="bold" />
<com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner
android:id="@+id/et_inCharset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_weight="1"
android:singleLine="true"
app:ms_entries="@array/charsets" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/out_charset"
android:textStyle="bold" />
<com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner
android:id="@+id/et_outCharset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_weight="1"
android:singleLine="true"
app:ms_entries="@array/charsets" />
</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_msgTemplate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional"
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_mqtt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/in_message_topic"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_inMessageTopic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/in_message_topic_hint"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/out_message_topic"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_outMessageTopic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/out_message_topic_hint"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/uri_type"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_uriType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/uri_type_hint"
android:singleLine="true"
app:met_clearButton="true" />
</LinearLayout>
<LinearLayout
style="@style/senderBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/client_id"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_clientId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/client_id_hint"
android:singleLine="true"
app:met_clearButton="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/path"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/path_hint"
android:singleLine="true"
app:met_clearButton="true" />
</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>

@ -664,7 +664,8 @@
<LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
tools:ignore="TooManyViews">
<LinearLayout
android:layout_width="0dp"

@ -331,7 +331,8 @@
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="10dp"
android:visibility="gone">
android:visibility="gone"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
@ -1056,6 +1057,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:digits="0123456789"
android:inputType="number"
android:maxLength="2"
android:maxLines="1"
@ -1085,6 +1087,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:digits="0123456789"
android:inputType="number"
android:maxLength="2"
android:maxLines="1"
@ -1114,6 +1117,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints=""
android:digits="0123456789"
android:inputType="number"
android:maxLength="2"
android:maxLines="1"

@ -143,4 +143,16 @@
<item>Beagle</item>
</string-array>
<string-array name="charsets">
<item>UTF-8</item>
<item>US-ASCII</item>
<item>ISO-8859-1</item>
<item>UTF-16</item>
<item>UTF-16BE</item>
<item>UTF-16LE</item>
<item>UTF-32</item>
<item>UTF-32LE</item>
<item>UTF-32BE</item>
</string-array>
</resources>

@ -398,6 +398,9 @@
<string name="get">GET</string>
<string name="put">PUT</string>
<string name="patch">PATCH</string>
<string name="udp">UDP</string>
<string name="tcp">TCP</string>
<string name="mqtt">MQTT</string>
<!--CloneActivity-->
<string name="local_ip">Local IP: </string>
<string name="operating_instruction">Instructions: \n[Note] The APP version of the sender and receiver must be the same!\n1. Please keep the SOURCE and DESTINATION phones in the same Wi-Fi network, and do not turn on isolation. \n2. Tap "Send" on SOURCE mobile phone, and get "server IP" \n3. After filling in "Server IP" on DESTINATION phone, tap "Receive". \n [NOTE:] sender(s), forwarding rule(s) and log(s) will be overwritten after cloning!</string> <!-- 原文是“新旧手机”,英文翻译中处理为“源”手机和“目标”手机,因为担心“新旧”的表述引起混淆(有没一种可能就是用户就是用从新手机的设备复制到旧手机上去呢?)。 -->
@ -410,8 +413,12 @@
<string name="old_mobile_phone">Old Phone</string>
<string name="new_mobile_phone">New Phone</string>
<string name="server_ip">Server IP: </string>
<string name="server_address">Host</string>
<string name="server_port">Port: </string>
<string name="point">.</string>
<string name="invalid_ip">Please enter a valid IP address</string>
<string name="invalid_ip">Please enter a valid IP or domain</string>
<string name="invalid_mqtt_message_topic">Please enter a valid message topic</string>
<string name="invalid_port">Please enter a valid port</string>
<string name="server_has_started">The server is started successfully</string>
<string name="server_has_stopped">The server has been stopped</string>
<string name="sender_cannot_receive">This mobile phone is the SOURCE and cannot receive files.</string>
@ -467,6 +474,18 @@
<string name="proxy_authenticator">Proxy Authenticator</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="in_charset">In Charset</string>
<string name="out_charset">Out Charset</string>
<string name="in_message_topic">In Message Topic</string>
<string name="in_message_topic_hint">Receive messages on the corresponding topic</string>
<string name="out_message_topic">Out Message Topic</string>
<string name="out_message_topic_hint">Send a message on the corresponding topic</string>
<string name="uri_type">Uri Type</string>
<string name="uri_type_hint">Optional, default is tcp</string>
<string name="path">Path</string>
<string name="path_hint">Used to set the uri when communicating using ws</string>
<string name="client_id">Client Id</string>
<string name="client_id_hint">Random value if empty</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="agree">Agree</string>
<string name="refuse">Refuse</string>
@ -706,10 +725,18 @@
<string name="url_scheme">URL Scheme</string>
<string name="url_scheme_tips">Examplemyapp://api/add?&amp;type=0&amp;msg=[msg]</string>
<string name="socket">Socket</string>
<string name="socket_tips">Examplemyapp://api/add?&amp;type=0&amp;msg=[msg]</string>
<string name="socket_address">服务地址</string>
<string name="socket_address_tips">IP 或 域名</string>
<string name="socket_port">端口</string>
<string name="socket_port_tips">取值范围1-65535</string>
<string name="webhook_server">Webhook Server</string>
<string name="webhook_server_tips">For example: https://a.b.com/msg?token=xyz</string>
<string name="webhook_params">Params</string>
<string name="webhook_params_tips" formatted="false">For example: payload=%7B%22text%22%3A%22[msg]%22%7D [msg] will be replaced with SMS content.\nJson format is supported, for example: {\"text\":\"[msg]\"}.\nNote: msg is automatically UTF-8 encoded except in JSON format</string>
<string name="msg_template">Msg Template</string>
<string name="webhook_params_tips" formatted="false">For example: payload=%7B%22text%22%3A%22[msg]%22%7D [msg] will be replaced with SMS content.\nJson format is supported, for example: {\"text\":\"[msg]\"}.\nNote: msg is automatically URLEncoder except in JSON format</string>
<string name="webhook_secret">Secret: If it is empty, the sign will not be calculated</string>
<string name="headers">Headers</string>
<string name="header_key">Key</string>

@ -143,4 +143,16 @@
<item>Beagle</item>
</string-array>
<string-array name="charsets">
<item>UTF-8</item>
<item>US-ASCII</item>
<item>ISO-8859-1</item>
<item>UTF-16</item>
<item>UTF-16BE</item>
<item>UTF-16LE</item>
<item>UTF-32</item>
<item>UTF-32LE</item>
<item>UTF-32BE</item>
</string-array>
</resources>

@ -399,6 +399,9 @@
<string name="get">GET</string>
<string name="put">PUT</string>
<string name="patch">PATCH</string>
<string name="udp">UDP</string>
<string name="tcp">TCP</string>
<string name="mqtt">MQTT</string>
<!--CloneActivity-->
<string name="local_ip">本机IP</string>
<string name="operating_instruction">严正声明:\n该功能仅限个人新旧手机切换使用用于非法用途后果自负\n\n操作说明\n1.新旧手机连接同一个WiFi网络(禁用AP隔离)如需穿透内网请先配置Frpc\n2.【二选一】旧手机点【推送】按钮,将本机的配置推送到服务端\n3.【二选一】新手机点【拉取】按钮,将拉取服务端的配置到本机\n\n注意事项\n1.客户端与服务端的APP版本必须一致才能克隆!\n2.导入成功后,发送通道、转发规则将完全被覆盖,清空历史记录!\n3.主动请求、保活措施、个性设置不在克隆范围</string>
@ -411,8 +414,12 @@
<string name="old_mobile_phone">我是旧手机</string>
<string name="new_mobile_phone">我是新手机</string>
<string name="server_ip">服务端IP</string>
<string name="server_address">主机:</string>
<string name="server_port">端口:</string>
<string name="point">.</string>
<string name="invalid_ip">请输入合法的IP地址</string>
<string name="invalid_ip">请输入合法的IP或域名</string>
<string name="invalid_mqtt_message_topic">请输入合法的输入/输出信息主题</string>
<string name="invalid_port">请输入合法的端口号</string>
<string name="server_has_started">服务端已启动</string>
<string name="server_has_stopped">服务端已停止</string>
<string name="sender_cannot_receive">本手机是发送端,不可接收文件,请先停止服务端!</string>
@ -468,6 +475,18 @@
<string name="proxy_authenticator">代理身份验证</string>
<string name="username">用户</string>
<string name="password">密码</string>
<string name="in_charset">输入编码</string>
<string name="out_charset">输出编码</string>
<string name="in_message_topic">输入消息主题</string>
<string name="in_message_topic_hint">接收对应主题的消息</string>
<string name="out_message_topic">输出消息主题</string>
<string name="out_message_topic_hint">发送对应主题的消息</string>
<string name="uri_type">通信方式</string>
<string name="uri_type_hint">可选默认为tcp</string>
<string name="path">通信路径</string>
<string name="path_hint">用于在使用ws进行通信时设置uri</string>
<string name="client_id">客户端ID</string>
<string name="client_id_hint">如果为空则为随机值</string>
<string name="privacy_policy">隐私政策</string>
<string name="agree">同意</string>
<string name="refuse">拒绝</string>
@ -707,10 +726,18 @@
<string name="url_scheme">URL Scheme</string>
<string name="url_scheme_tips">示例myapp://api/add?&amp;type=0&amp;msg=[msg]</string>
<string name="socket">Socket</string>
<string name="socket_tips">示例myapp://api/add?&amp;type=0&amp;msg=[msg]</string>
<string name="socket_address">服务地址</string>
<string name="socket_address_tips">IP 或 域名</string>
<string name="socket_port">端口</string>
<string name="socket_port_tips">取值范围1-65535</string>
<string name="webhook_server">Webhook Server</string>
<string name="webhook_server_tips">例如https://a.b.com/msg?token=xyz</string>
<string name="webhook_params">Params</string>
<string name="webhook_params_tips" formatted="false">例如payload=%7B%22text%22%3A%22[msg]%22%7D [msg]将被替换成短信内容。\n支持Json格式例如{\"text\":\"[msg]\"}。\n注意除JSON格式外msg会自动进行UTF-8编码</string>
<string name="webhook_params">消息模板</string>
<string name="msg_template">Params</string>
<string name="webhook_params_tips" formatted="false">例如payload=%7B%22text%22%3A%22[msg]%22%7D [msg]将被替换成短信内容。\n支持Json格式例如{\"text\":\"[msg]\"}。\n注意除JSON格式外msg会自动进行URLEncoder</string>
<string name="webhook_secret">Secret置空则不计算sign</string>
<string name="headers">Headers</string>
<string name="header_key">Key</string>

Loading…
Cancel
Save