mirror of
https://github.com/pppscn/SmsForwarder
synced 2024-11-08 01:10:23 +00:00
优化:发送通道Webhook
支持http/socks5代理
优化:服务端应答`Http 204 No Content`时特殊处理(更新日志状态为成功) #234
This commit is contained in:
parent
9107fa4589
commit
5eed98121e
@ -2,21 +2,36 @@ package com.idormy.sms.forwarder.entity.setting
|
||||
|
||||
import com.idormy.sms.forwarder.R
|
||||
import java.io.Serializable
|
||||
import java.net.Proxy
|
||||
|
||||
data class WebhookSetting(
|
||||
val method: String? = "POST",
|
||||
val method: String = "POST",
|
||||
var webServer: String = "",
|
||||
val secret: String? = "",
|
||||
val response: String? = "",
|
||||
val webParams: String? = "",
|
||||
val headers: Map<String, String>?,
|
||||
val secret: String = "",
|
||||
val response: String = "",
|
||||
val webParams: String = "",
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
val proxyType: Proxy.Type = Proxy.Type.DIRECT,
|
||||
val proxyHost: String = "",
|
||||
val proxyPort: String = "",
|
||||
val proxyAuthenticator: Boolean = false,
|
||||
val proxyUsername: String = "",
|
||||
val proxyPassword: String = "",
|
||||
) : Serializable {
|
||||
fun getMethodCheckId(): Int {
|
||||
return when (method) {
|
||||
null, "POST" -> R.id.rb_method_post
|
||||
"POST" -> R.id.rb_method_post
|
||||
"PUT" -> R.id.rb_method_put
|
||||
"PATCH" -> R.id.rb_method_patch
|
||||
else -> R.id.rb_method_get
|
||||
}
|
||||
}
|
||||
|
||||
fun getProxyTypeCheckId(): Int {
|
||||
return when (proxyType) {
|
||||
Proxy.Type.HTTP -> R.id.rb_proxyHttp
|
||||
Proxy.Type.SOCKS -> R.id.rb_proxySocks
|
||||
else -> R.id.rb_proxyNone
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,11 @@ import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.gson.Gson
|
||||
import com.idormy.sms.forwarder.R
|
||||
@ -41,11 +43,12 @@ import io.reactivex.SingleObserver
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.net.Proxy
|
||||
import java.util.Date
|
||||
|
||||
@Page(name = "Webhook")
|
||||
@Suppress("PrivatePropertyName")
|
||||
class WebhookFragment : BaseFragment<FragmentSendersWebhookBinding?>(), View.OnClickListener {
|
||||
class WebhookFragment : BaseFragment<FragmentSendersWebhookBinding?>(), View.OnClickListener, CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
private val TAG: String = WebhookFragment::class.java.simpleName
|
||||
private var titleBar: TitleBar? = null
|
||||
@ -132,13 +135,17 @@ class WebhookFragment : BaseFragment<FragmentSendersWebhookBinding?>(), View.OnC
|
||||
binding!!.etResponse.setText(settingVo.response)
|
||||
binding!!.etWebParams.setText(settingVo.webParams)
|
||||
//set header
|
||||
if (settingVo.headers != null) {
|
||||
for ((key, value) in settingVo.headers) {
|
||||
addHeaderItemLinearLayout(
|
||||
headerItemMap, binding!!.layoutHeaders, key, value
|
||||
)
|
||||
}
|
||||
}
|
||||
binding!!.rgProxyType.check(settingVo.getProxyTypeCheckId())
|
||||
binding!!.etProxyHost.setText(settingVo.proxyHost)
|
||||
binding!!.etProxyPort.setText(settingVo.proxyPort)
|
||||
binding!!.sbProxyAuthenticator.isChecked = settingVo.proxyAuthenticator == true
|
||||
binding!!.etProxyUsername.setText(settingVo.proxyUsername)
|
||||
binding!!.etProxyPassword.setText(settingVo.proxyPassword)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -151,9 +158,26 @@ class WebhookFragment : BaseFragment<FragmentSendersWebhookBinding?>(), View.OnC
|
||||
binding!!.btnAddHeader.setOnClickListener {
|
||||
addHeaderItemLinearLayout(headerItemMap, binding!!.layoutHeaders, null, null)
|
||||
}
|
||||
binding!!.sbProxyAuthenticator.setOnCheckedChangeListener(this)
|
||||
binding!!.rgProxyType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
|
||||
if (checkedId == R.id.rb_proxyHttp || checkedId == R.id.rb_proxySocks) {
|
||||
binding!!.layoutProxyHost.visibility = View.VISIBLE
|
||||
binding!!.layoutProxyPort.visibility = View.VISIBLE
|
||||
binding!!.layoutProxyAuthenticator.visibility = if (binding!!.sbProxyAuthenticator.isChecked) View.VISIBLE else View.GONE
|
||||
} else {
|
||||
binding!!.layoutProxyHost.visibility = View.GONE
|
||||
binding!!.layoutProxyPort.visibility = View.GONE
|
||||
binding!!.layoutProxyAuthenticator.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() }
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
|
||||
//注意:因为只有一个监听,暂不需要判断id
|
||||
binding!!.layoutProxyAuthenticator.visibility = if (isChecked) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@SingleClick
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
@ -233,7 +257,26 @@ class WebhookFragment : BaseFragment<FragmentSendersWebhookBinding?>(), View.OnC
|
||||
val webParams = binding!!.etWebParams.text.toString().trim()
|
||||
val headers = getHeadersFromHeaderItemMap(headerItemMap)
|
||||
|
||||
return WebhookSetting(method, webServer, secret, response, webParams, headers)
|
||||
val proxyType: Proxy.Type = when (binding!!.rgProxyType.checkedRadioButtonId) {
|
||||
R.id.rb_proxyHttp -> Proxy.Type.HTTP
|
||||
R.id.rb_proxySocks -> Proxy.Type.SOCKS
|
||||
else -> Proxy.Type.DIRECT
|
||||
}
|
||||
val proxyHost = binding!!.etProxyHost.text.toString().trim()
|
||||
val proxyPort = binding!!.etProxyPort.text.toString().trim()
|
||||
|
||||
if (proxyType != Proxy.Type.DIRECT && (TextUtils.isEmpty(proxyHost) || TextUtils.isEmpty(proxyPort))) {
|
||||
throw Exception(getString(R.string.invalid_host_or_port))
|
||||
}
|
||||
|
||||
val proxyAuthenticator = binding!!.sbProxyAuthenticator.isChecked
|
||||
val proxyUsername = binding!!.etProxyUsername.text.toString().trim()
|
||||
val proxyPassword = binding!!.etProxyPassword.text.toString().trim()
|
||||
if (proxyAuthenticator && TextUtils.isEmpty(proxyUsername) && TextUtils.isEmpty(proxyPassword)) {
|
||||
throw Exception(getString(R.string.invalid_username_or_password))
|
||||
}
|
||||
|
||||
return WebhookSetting(method, webServer, secret, response, webParams, headers, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword)
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
package com.idormy.sms.forwarder.utils.interceptor
|
||||
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import com.google.gson.Gson
|
||||
import com.idormy.sms.forwarder.entity.result.SendResponse
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import com.idormy.sms.forwarder.utils.Worker
|
||||
import com.idormy.sms.forwarder.workers.UpdateLogsWorker
|
||||
import com.xuexiang.xutil.XUtil
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NoContentInterceptor(private val logId: Long) : Interceptor {
|
||||
|
||||
private val TAG: String = NoContentInterceptor::class.java.simpleName
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
|
||||
if (originalResponse.code() == 204) {
|
||||
val response = "HTTP 204 No Content"
|
||||
Log.d(TAG, response)
|
||||
/*
|
||||
// 创建一个空的响应体
|
||||
val message = "{\"Code\":0, \"Msg\":\"\", \"Data\":{}}"
|
||||
val emptyJsonBody = ResponseBody.create(MediaType.parse("application/json"), message)
|
||||
// 使用新的响应体替换原始响应中的响应体
|
||||
return originalResponse.newBuilder()
|
||||
.body(emptyJsonBody)
|
||||
.header("Content-Length", message.length.toString())
|
||||
.build()
|
||||
*/
|
||||
//TODO: 暂时特殊处理,更新日志状态为成功
|
||||
val sendResponse = SendResponse(logId, 2, response)
|
||||
val request = OneTimeWorkRequestBuilder<UpdateLogsWorker>()
|
||||
.setInitialDelay(200, TimeUnit.MILLISECONDS)
|
||||
.setInputData(
|
||||
workDataOf(
|
||||
Worker.UPDATE_LOGS to Gson().toJson(sendResponse)
|
||||
)
|
||||
).build()
|
||||
WorkManager.getInstance(XUtil.getContext()).enqueue(request)
|
||||
}
|
||||
|
||||
return originalResponse
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import com.google.gson.Gson
|
||||
import com.idormy.sms.forwarder.R
|
||||
import com.idormy.sms.forwarder.database.entity.Rule
|
||||
import com.idormy.sms.forwarder.entity.MsgInfo
|
||||
import com.idormy.sms.forwarder.entity.setting.WebhookSetting
|
||||
@ -11,16 +12,29 @@ import com.idormy.sms.forwarder.utils.AppUtils
|
||||
import com.idormy.sms.forwarder.utils.Log
|
||||
import com.idormy.sms.forwarder.utils.SendUtils
|
||||
import com.idormy.sms.forwarder.utils.SettingUtils
|
||||
import com.idormy.sms.forwarder.utils.interceptor.BasicAuthInterceptor
|
||||
import com.idormy.sms.forwarder.utils.interceptor.LoggingInterceptor
|
||||
import com.idormy.sms.forwarder.utils.interceptor.NoContentInterceptor
|
||||
import com.xuexiang.xhttp2.XHttp
|
||||
import com.xuexiang.xhttp2.callback.SimpleCallBack
|
||||
import com.xuexiang.xhttp2.exception.ApiException
|
||||
import com.xuexiang.xutil.net.NetworkUtils
|
||||
import com.xuexiang.xutil.resource.ResUtils
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import java.net.Authenticator
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.Proxy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
class WebhookUtils {
|
||||
companion object {
|
||||
|
||||
@ -57,7 +71,7 @@ class WebhookUtils {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(
|
||||
SecretKeySpec(
|
||||
setting.secret?.toByteArray(StandardCharsets.UTF_8),
|
||||
setting.secret.toByteArray(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
)
|
||||
)
|
||||
@ -65,7 +79,7 @@ class WebhookUtils {
|
||||
sign = URLEncoder.encode(String(Base64.encode(signData, Base64.NO_WRAP)), "UTF-8")
|
||||
}
|
||||
|
||||
var webParams = setting.webParams?.trim()
|
||||
var webParams = setting.webParams.trim()
|
||||
|
||||
//支持HTTP基本认证(Basic Authentication)
|
||||
val regex = "^(https?://)([^:]+):([^@]+)@(.+)"
|
||||
@ -90,7 +104,7 @@ class WebhookUtils {
|
||||
Log.d(TAG, "method = GET, Url = $requestUrl")
|
||||
XHttp.get(requestUrl).keepJson(true)
|
||||
} else if (setting.method == "GET" && !TextUtils.isEmpty(webParams)) {
|
||||
webParams = webParams.toString().replace("[from]", URLEncoder.encode(from, "UTF-8"))
|
||||
webParams = webParams.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"))
|
||||
@ -114,7 +128,7 @@ class WebhookUtils {
|
||||
}
|
||||
Log.d(TAG, "method = GET, Url = $requestUrl")
|
||||
XHttp.get(requestUrl).keepJson(true)
|
||||
} else if (!webParams.isNullOrEmpty() && webParams.startsWith("{")) {
|
||||
} else if (webParams.isNotEmpty() && webParams.startsWith("{")) {
|
||||
val bodyMsg = webParams.replace("[from]", from)
|
||||
.replace("[content]", escapeJson(content))
|
||||
.replace("[msg]", escapeJson(content))
|
||||
@ -136,7 +150,7 @@ class WebhookUtils {
|
||||
else -> XHttp.post(requestUrl).keepJson(true).upJson(bodyMsg)
|
||||
}
|
||||
} else {
|
||||
if (webParams.isNullOrEmpty()) {
|
||||
if (webParams.isEmpty()) {
|
||||
webParams = "from=[from]&content=[content]×tamp=[timestamp]"
|
||||
if (!TextUtils.isEmpty(sign)) webParams += "&sign=[sign]"
|
||||
}
|
||||
@ -171,7 +185,7 @@ class WebhookUtils {
|
||||
}
|
||||
|
||||
//添加headers
|
||||
for ((key, value) in setting.headers?.entries!!) {
|
||||
for ((key, value) in setting.headers.entries) {
|
||||
request.headers(key, value)
|
||||
}
|
||||
|
||||
@ -180,24 +194,65 @@ class WebhookUtils {
|
||||
request.addInterceptor(BasicAuthInterceptor(matches[2], matches[3]))
|
||||
}
|
||||
|
||||
//设置代理
|
||||
if ((setting.proxyType == Proxy.Type.HTTP || setting.proxyType == Proxy.Type.SOCKS)
|
||||
&& !TextUtils.isEmpty(setting.proxyHost) && !TextUtils.isEmpty(setting.proxyPort)
|
||||
) {
|
||||
//代理服务器的IP和端口号
|
||||
Log.d(TAG, "proxyHost = ${setting.proxyHost}, proxyPort = ${setting.proxyPort}")
|
||||
val proxyHost = if (NetworkUtils.isIP(setting.proxyHost)) setting.proxyHost else NetworkUtils.getDomainAddress(setting.proxyHost)
|
||||
if (!NetworkUtils.isIP(proxyHost)) {
|
||||
throw Exception(String.format(ResUtils.getString(R.string.invalid_proxy_host), proxyHost))
|
||||
}
|
||||
val proxyPort: Int = setting.proxyPort.toInt()
|
||||
|
||||
Log.d(TAG, "proxyHost = $proxyHost, proxyPort = $proxyPort")
|
||||
request.okproxy(Proxy(setting.proxyType, InetSocketAddress(proxyHost, proxyPort)))
|
||||
|
||||
//代理的鉴权账号密码
|
||||
if (setting.proxyAuthenticator && (!TextUtils.isEmpty(setting.proxyUsername) || !TextUtils.isEmpty(setting.proxyPassword))
|
||||
) {
|
||||
Log.i(TAG, "proxyUsername = ${setting.proxyUsername}, proxyPassword = ${setting.proxyPassword}")
|
||||
|
||||
if (setting.proxyType == Proxy.Type.HTTP) {
|
||||
request.okproxyAuthenticator { _: Route?, response: Response ->
|
||||
//设置代理服务器账号密码
|
||||
val credential = Credentials.basic(setting.proxyUsername, setting.proxyPassword)
|
||||
response.request().newBuilder()
|
||||
.header("Proxy-Authorization", credential)
|
||||
.build()
|
||||
}
|
||||
} else {
|
||||
Authenticator.setDefault(object : Authenticator() {
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
||||
return PasswordAuthentication(setting.proxyUsername, setting.proxyPassword.toCharArray())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.ignoreHttpsCert() //忽略https证书
|
||||
.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
|
||||
.retryDelay(SettingUtils.requestDelayTime * 1000) //超时重试的延迟时间
|
||||
.retryIncreaseDelay(SettingUtils.requestDelayTime * 1000) //超时重试叠加延时
|
||||
.timeStamp(true) //url自动追加时间戳,避免缓存
|
||||
.addInterceptor(LoggingInterceptor(logId)) //增加一个log拦截器, 记录请求日志
|
||||
.execute(object : SimpleCallBack<String>() {
|
||||
.addInterceptor(NoContentInterceptor(logId)) //拦截 HTTP 204 响应
|
||||
.execute(object : SimpleCallBack<Any>() {
|
||||
|
||||
override fun onError(e: ApiException) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, e.detailMessage)
|
||||
val status = 0
|
||||
SendUtils.updateLogs(logId, status, e.displayMessage)
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
||||
override fun onSuccess(response: String) {
|
||||
override fun onSuccess(resp: Any) {
|
||||
val response = resp.toString()
|
||||
Log.i(TAG, response)
|
||||
val status = if (!setting.response.isNullOrEmpty() && !response.contains(setting.response)) 0 else 2
|
||||
val status = if (setting.response.isNotEmpty() && !response.contains(setting.response)) 0 else 2
|
||||
SendUtils.updateLogs(logId, status, response)
|
||||
SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId)
|
||||
}
|
||||
|
@ -248,6 +248,153 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
style="@style/BarStyle"
|
||||
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/proxy_settings"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_proxyType"
|
||||
style="@style/rg_style"
|
||||
android:layout_marginStart="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_proxyNone"
|
||||
style="@style/rg_rb_style"
|
||||
android:checked="true"
|
||||
android:text="@string/proxy_none" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_proxyHttp"
|
||||
style="@style/rg_rb_style"
|
||||
android:text="@string/proxy_http" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_proxySocks"
|
||||
style="@style/rg_rb_style"
|
||||
android:text="@string/proxy_socks" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutProxyHost"
|
||||
style="@style/BarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hostname" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_proxyHost"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
app:met_clearButton="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutProxyPort"
|
||||
style="@style/BarStyle.Switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/port" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_proxyPort"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:text="@string/proxy_authenticator" />
|
||||
|
||||
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
|
||||
android:id="@+id/sb_proxyAuthenticator"
|
||||
style="@style/SwitchButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutProxyAuthenticator"
|
||||
style="@style/BarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
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" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_proxyUsername"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
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" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_proxyPassword"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
app:met_passWordButton="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
Loading…
Reference in New Issue
Block a user