mirror of
https://github.com/pppscn/SmsForwarder
synced 2024-11-04 06:00:11 +00:00
parent
354393a231
commit
2cccb9b4fa
@ -192,7 +192,7 @@ dependencies {
|
||||
//kmnkt基于Kotlin Multiplatform的跨平台socket通信统一接口,支持UDP/TCP/MQTT协议
|
||||
//https://github.com/xuankaicat/kmnkt
|
||||
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
|
||||
implementation files('libs/socket-2.0.0-alpha06-2.aar')
|
||||
//implementation files('libs/socket-2.0.0-alpha06-2.aar')
|
||||
|
||||
testImplementation deps.junit
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
Binary file not shown.
@ -9,6 +9,7 @@ data class SocketSetting(
|
||||
val port: Int = 0, //端口号
|
||||
val msgTemplate: String = "", //消息模板
|
||||
val secret: String? = "", //签名密钥
|
||||
val response: String? = "", //成功应答关键字
|
||||
val username: String = "", //用户名
|
||||
val password: String = "", //密码
|
||||
val inCharset: String = "", //输入编码
|
||||
@ -29,4 +30,11 @@ data class SocketSetting(
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriTypeCheckId(): Int {
|
||||
return when (method) {
|
||||
"ssl" -> R.id.rb_uriType_ssl
|
||||
else -> R.id.rb_uriType_tcp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -122,13 +122,14 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
|
||||
binding!!.etPort.setText(settingVo.port.toString())
|
||||
binding!!.etMsgTemplate.setText(settingVo.msgTemplate)
|
||||
binding!!.etSecret.setText(settingVo.secret)
|
||||
binding!!.etResponse.setText(settingVo.response)
|
||||
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!!.rgUriType.check(settingVo.getUriTypeCheckId())
|
||||
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
|
||||
@ -170,6 +171,7 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
|
||||
}.start()
|
||||
return
|
||||
}
|
||||
|
||||
R.id.btn_del -> {
|
||||
if (senderId <= 0 || isClone) {
|
||||
popToBack()
|
||||
@ -183,6 +185,7 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
R.id.btn_save -> {
|
||||
val name = binding!!.etName.text.toString().trim()
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
@ -226,13 +229,17 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
|
||||
|
||||
val msgTemplate = binding!!.etMsgTemplate.text.toString().trim()
|
||||
val secret = binding!!.etSecret.text.toString().trim()
|
||||
val response = binding!!.etResponse.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 uriType = when (binding!!.rgUriType.checkedRadioButtonId) {
|
||||
R.id.rb_uriType_ssl -> "ssl"
|
||||
else -> "tcp"
|
||||
}
|
||||
val path = binding!!.etPath.text.toString().trim()
|
||||
val clientId = binding!!.etClientId.text.toString().trim()
|
||||
|
||||
@ -240,7 +247,7 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
|
||||
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)
|
||||
return SocketSetting(method, address, port.toInt(), msgTemplate, secret, response, username, password, inCharset, outCharset, inMessageTopic, outMessageTopic, uriType, path, clientId)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -4,12 +4,6 @@ import android.annotation.SuppressLint
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
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
|
||||
@ -17,7 +11,20 @@ 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 org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
|
||||
import org.eclipse.paho.client.mqttv3.MqttException
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.Socket
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@ -62,113 +69,100 @@ class SocketUtils {
|
||||
}
|
||||
|
||||
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)//设置输出编码
|
||||
}
|
||||
}
|
||||
// 创建套接字并连接到服务器
|
||||
val socket = Socket(setting.address, setting.port)
|
||||
Log.d(TAG, "连接到服务器: ${setting.address}:${setting.port}")
|
||||
try {
|
||||
// 获取输入流和输出流,设置字符集为UTF-8
|
||||
val input = BufferedReader(InputStreamReader(socket.getInputStream(), Charset.forName(setting.inCharset)))
|
||||
val output = BufferedWriter(OutputStreamWriter(socket.getOutputStream(), Charset.forName(setting.outCharset)))
|
||||
|
||||
socket.open {
|
||||
success {
|
||||
Log.d(TAG, "${setting.method}连接成功")
|
||||
isConnected = true
|
||||
//SendUtils.updateLogs(logId, 1, "TCP连接成功")
|
||||
socket.send(message)
|
||||
socket.startReceive { str, data ->
|
||||
isReceived = true
|
||||
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 {
|
||||
Log.d(TAG, "${setting.method}连接失败")
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, "TCP连接失败")
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
return@failure false//是否继续尝试连接
|
||||
}
|
||||
loss {
|
||||
Log.d(TAG, "${setting.method}连接断开")
|
||||
return@loss true//是否尝试重连
|
||||
}
|
||||
}
|
||||
// 向服务器发送数据
|
||||
output.write(message)
|
||||
output.newLine() // 添加换行符以便服务器使用readLine()来读取
|
||||
output.flush()
|
||||
Log.d(TAG, "发送到服务器的消息: $message")
|
||||
|
||||
//延时5秒关闭连接
|
||||
if (isConnected) {
|
||||
Thread.sleep(10000)
|
||||
socket.stopReceive()
|
||||
// 从服务器接收响应
|
||||
val response = input.readLine()
|
||||
Log.d(TAG, "从服务器接收的响应: $response")
|
||||
val status = if (!setting.response.isNullOrEmpty() && !response.contains(setting.response)) 0 else 2
|
||||
SendUtils.updateLogs(logId, status, response)
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, e.message.toString())
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
} finally {
|
||||
// 关闭套接字
|
||||
socket.close()
|
||||
if (!isReceived) {
|
||||
SendUtils.updateLogs(logId, 0, "未收到订阅消息")
|
||||
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
Log.d(TAG, "Disconnected from MQTT broker")
|
||||
}
|
||||
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 连接参数
|
||||
val uriType = if (TextUtils.isEmpty(setting.uriType)) "tcp" else setting.uriType
|
||||
val brokerUrl = "${uriType}://${setting.address}:${setting.port}"
|
||||
if (!TextUtils.isEmpty(setting.path)) {
|
||||
brokerUrl.plus(setting.path)
|
||||
}
|
||||
Log.d(TAG, "MQTT brokerUrl: $brokerUrl")
|
||||
val clientId = if (TextUtils.isEmpty(setting.clientId)) UUID.randomUUID().toString() else setting.clientId
|
||||
val mqttClient = MqttClient(brokerUrl, clientId, MemoryPersistence())
|
||||
try {
|
||||
val options = MqttConnectOptions()
|
||||
if (!TextUtils.isEmpty(setting.username)) {
|
||||
options.userName = setting.username
|
||||
}
|
||||
if (!TextUtils.isEmpty(setting.password)) {
|
||||
options.password = setting.password.toCharArray()
|
||||
}
|
||||
options.isCleanSession = true
|
||||
mqttClient.connect(options)
|
||||
Log.d(TAG, "Connected to MQTT broker: ${mqttClient.serverURI}")
|
||||
|
||||
mqtt.open {
|
||||
success {
|
||||
Log.d(TAG, "MQTT连接成功")
|
||||
//SendUtils.updateLogs(logId, 1, "MQTT连接成功")
|
||||
// 订阅并发布后等待至拿到响应消息并赋值给result
|
||||
// 如果超过10秒没有收到消息则将result设为"消息响应超时",并取消订阅topic
|
||||
val response = mqtt.sendAndReceiveSync(setting.outMessageTopic, setting.inMessageTopic, message, 10000L) ?: "消息响应超时"
|
||||
mqtt.close()
|
||||
mqttClient.subscribe(setting.inMessageTopic)
|
||||
Log.d(TAG, "Subscribed to topic: $setting.inMessageTopic")
|
||||
|
||||
val status = if (response == "消息响应超时") 0 else 2
|
||||
SendUtils.updateLogs(logId, status, "收到订阅消息:$response")
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
return@success
|
||||
}
|
||||
failure {
|
||||
Log.d(TAG, "MQTT连接失败")
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, "MQTT连接失败")
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
return@failure false//是否继续尝试连接
|
||||
}
|
||||
loss {
|
||||
Log.d(TAG, "MQTT失去连接")
|
||||
return@loss true//是否尝试重连
|
||||
}
|
||||
val outMessage = message.toByteArray(Charset.forName(setting.outCharset))
|
||||
val mqttMessage = MqttMessage(outMessage)
|
||||
mqttMessage.qos = 0 // 设置消息质量服务等级
|
||||
//异步发布消息
|
||||
mqttClient.publish(setting.outMessageTopic, mqttMessage)
|
||||
Log.d(TAG, "Published message to topic: $setting.outMessageTopic")
|
||||
mqttClient.setCallback(object : MqttCallbackExtended {
|
||||
override fun connectionLost(cause: Throwable?) {
|
||||
val response = "Connection to MQTT broker lost: ${cause?.message}"
|
||||
Log.d(TAG, response)
|
||||
SendUtils.updateLogs(logId, 0, response)
|
||||
SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String?, inMessage: MqttMessage?) {
|
||||
val payload = inMessage?.payload?.toString(Charset.forName(setting.inCharset))
|
||||
Log.d(TAG, "Received message on topic $topic: $payload")
|
||||
val status = if (!setting.response.isNullOrEmpty() && !payload?.contains(setting.response)!!) 0 else 2
|
||||
SendUtils.updateLogs(logId, status, payload.toString())
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
Log.d(TAG, "deliveryComplete")
|
||||
SendUtils.updateLogs(logId, 2, "deliveryComplete")
|
||||
SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
||||
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
|
||||
Log.d(TAG, "connectComplete")
|
||||
}
|
||||
})
|
||||
} catch (e: MqttException) {
|
||||
Log.d(TAG, "An error occurred: ${e.message}")
|
||||
} finally {
|
||||
mqttClient.disconnect()
|
||||
Log.d(TAG, "Disconnected from MQTT broker")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//JSON需要转义的字符
|
||||
|
@ -267,6 +267,28 @@
|
||||
|
||||
</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_response2"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_Response"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/optional"
|
||||
android:singleLine="true"
|
||||
app:met_clearButton="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_mqtt"
|
||||
android:layout_width="match_parent"
|
||||
@ -333,15 +355,28 @@
|
||||
android:text="@string/uri_type"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_uriType"
|
||||
android:layout_width="0dp"
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_uriType"
|
||||
android:layout_width="wrap_content"
|
||||
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" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_uriType_tcp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tcp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_uriType_ssl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ssl"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -411,6 +411,7 @@
|
||||
<string name="udp">UDP</string>
|
||||
<string name="tcp">TCP</string>
|
||||
<string name="mqtt">MQTT</string>
|
||||
<string name="ssl">SSL</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> <!-- 原文是“新旧手机”,英文翻译中处理为“源”手机和“目标”手机,因为担心“新旧”的表述引起混淆(有没一种可能就是用户就是用从新手机的设备复制到旧手机上去呢?)。 -->
|
||||
@ -780,6 +781,7 @@
|
||||
<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, e.g. {\"text\":\"[msg]\"}.\nNote: msg is automatically URLEncoder except in JSON format</string>
|
||||
<string name="webhook_secret">Secret: If left empty, the sign will not be calculated</string>
|
||||
<string name="webhook_response">Successful Response Keyword:If left empty, HTTP status 200 represents success</string>
|
||||
<string name="webhook_response2">Successful Response Keyword:Leaving it blank means sending is considered as successful.</string>
|
||||
<string name="headers">Headers</string>
|
||||
<string name="header_key">Key</string>
|
||||
<string name="header_value">Value</string>
|
||||
|
@ -412,6 +412,7 @@
|
||||
<string name="udp">UDP</string>
|
||||
<string name="tcp">TCP</string>
|
||||
<string name="mqtt">MQTT</string>
|
||||
<string name="ssl">SSL</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>
|
||||
@ -781,6 +782,7 @@
|
||||
<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="webhook_response">成功应答关键字:置空则http状态200即为成功</string>
|
||||
<string name="webhook_response2">成功应答关键字:置空则发出即成功</string>
|
||||
<string name="headers">Headers</string>
|
||||
<string name="header_key">Key</string>
|
||||
<string name="header_value">Value</string>
|
||||
|
Loading…
Reference in New Issue
Block a user