You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SmsForwarder/app/src/main/java/com/idormy/sms/forwarder/fragment/ClientFragment.kt

410 lines
20 KiB
Kotlin

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.idormy.sms.forwarder.fragment
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioGroup
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.WidgetItemAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientBinding
import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.server.model.ConfigData
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.CLIENT_FRAGMENT_LIST
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.RSACrypt
import com.idormy.sms.forwarder.utils.SM4Crypt
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.data.ConvertTools
@Suppress("PrivatePropertyName")
@Page(name = "主动控制·客户端")
class ClientFragment : BaseFragment<FragmentClientBinding?>(), View.OnClickListener, RecyclerViewHolder.OnItemClickListener<PageInfo> {
private val TAG: String = ClientFragment::class.java.simpleName
private var appContext: App? = null
private var serverConfig: ConfigData? = null
private var serverHistory: MutableMap<String, String> = mutableMapOf()
private var mCountDownHelper: CountDownButtonHelper? = null
override fun initViews() {
appContext = requireActivity().application as App
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnServerTest, 3)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnServerTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnServerTest.text = getString(R.string.server_test)
}
})
WidgetUtils.initGridRecyclerView(binding!!.recyclerView, 3, DensityUtils.dp2px(1f))
val widgetItemAdapter = WidgetItemAdapter(CLIENT_FRAGMENT_LIST)
widgetItemAdapter.setOnItemClickListener(this)
binding!!.recyclerView.adapter = widgetItemAdapter
//取出历史记录
val history = HttpServerUtils.serverHistory
if (!TextUtils.isEmpty(history)) {
serverHistory = Gson().fromJson(history, object : TypeToken<MutableMap<String, String>>() {}.type)
}
if (CommonUtils.checkUrl(HttpServerUtils.serverAddress)) queryConfig(false)
}
override fun initTitle(): TitleBar? {
val titleBar = super.initTitle()!!.setImmersive(false)
//纯客户端模式
if (SettingUtils.enablePureClientMode) {
titleBar.setTitle(R.string.app_name).setSubTitle(getString(R.string.menu_client)).disableLeftView()
titleBar.addAction(object : TitleBar.ImageAction(R.drawable.ic_logout) {
@SingleClick
override fun performAction(view: View) {
XToastUtils.success(getString(R.string.exit_pure_client_mode))
SettingUtils.enablePureClientMode = false
try {
Thread.sleep(500) //延迟500毫秒避免退出时enablePureClientMode还没保存
XUtil.exitApp()
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
})
} else {
titleBar.setTitle(R.string.menu_client)
}
return titleBar
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentClientBinding {
return FragmentClientBinding.inflate(inflater, container, false)
}
override fun initListeners() {
binding!!.etServerAddress.setText(HttpServerUtils.serverAddress)
binding!!.etServerAddress.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
HttpServerUtils.serverAddress = binding!!.etServerAddress.text.toString().trim().trimEnd('/')
}
})
//安全措施
var safetyMeasuresId = R.id.rb_safety_measures_none
when (HttpServerUtils.clientSafetyMeasures) {
1 -> {
safetyMeasuresId = R.id.rb_safety_measures_sign
binding!!.tvSignKey.text = getString(R.string.sign_key)
}
2 -> {
safetyMeasuresId = R.id.rb_safety_measures_rsa
binding!!.tvSignKey.text = getString(R.string.public_key)
}
3 -> {
safetyMeasuresId = R.id.rb_safety_measures_sm4
binding!!.tvSignKey.text = getString(R.string.sm4_key)
}
else -> {
binding!!.layoutSignKey.visibility = View.GONE
}
}
binding!!.rgSafetyMeasures.check(safetyMeasuresId)
binding!!.rgSafetyMeasures.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
var safetyMeasures = 0
binding!!.layoutSignKey.visibility = View.VISIBLE
when (checkedId) {
R.id.rb_safety_measures_sign -> {
safetyMeasures = 1
binding!!.tvSignKey.text = getString(R.string.sign_key)
}
R.id.rb_safety_measures_rsa -> {
safetyMeasures = 2
binding!!.tvSignKey.text = getString(R.string.public_key)
}
R.id.rb_safety_measures_sm4 -> {
safetyMeasures = 3
binding!!.tvSignKey.text = getString(R.string.sm4_key)
}
else -> {
binding!!.layoutSignKey.visibility = View.GONE
}
}
HttpServerUtils.clientSafetyMeasures = safetyMeasures
}
binding!!.etSignKey.setText(HttpServerUtils.clientSignKey)
binding!!.etSignKey.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
HttpServerUtils.clientSignKey = binding!!.etSignKey.text.toString().trim()
}
})
binding!!.btnWechatMiniprogram.setOnClickListener(this)
binding!!.btnServerHistory.setOnClickListener(this)
binding!!.btnServerTest.setOnClickListener(this)
}
@SingleClick
override fun onClick(v: View) {
when (v.id) {
R.id.btn_wechat_miniprogram -> {
if (HttpServerUtils.safetyMeasures != 3) {
XToastUtils.error("微信小程序只支持SM4加密传输请前往主动控制·服务端修改安全措施")
return
}
CommonUtils.previewPicture(this, getString(R.string.url_wechat_miniprogram), null)
}
R.id.btn_server_history -> {
if (serverHistory.isEmpty()) {
XToastUtils.warning(getString(R.string.no_server_history))
return
}
Log.d(TAG, "serverHistory = $serverHistory")
MaterialDialog.Builder(requireContext()).title(R.string.server_history).items(serverHistory.keys).itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
//XToastUtils.info("$which: $text")
val matches = Regex("【(.*)】(.*)", RegexOption.IGNORE_CASE).findAll(text).toList().flatMap(MatchResult::groupValues)
Log.i(TAG, "matches = $matches")
if (matches.isNotEmpty()) {
binding!!.etServerAddress.setText(matches[2])
} else {
binding!!.etServerAddress.setText(text)
}
val signKey = serverHistory[text].toString()
if (!TextUtils.isEmpty(signKey)) {
val keyMatches = Regex("(.*)##(.*)", RegexOption.IGNORE_CASE).findAll(signKey).toList().flatMap(MatchResult::groupValues)
Log.i(TAG, "keyMatches = $keyMatches")
if (keyMatches.isNotEmpty()) {
binding!!.etSignKey.setText(keyMatches[1])
var safetyMeasuresId = R.id.rb_safety_measures_none
when (keyMatches[2]) {
"1" -> {
safetyMeasuresId = R.id.rb_safety_measures_sign
binding!!.tvSignKey.text = getString(R.string.sign_key)
}
"2" -> {
safetyMeasuresId = R.id.rb_safety_measures_rsa
binding!!.tvSignKey.text = getString(R.string.public_key)
}
"3" -> {
safetyMeasuresId = R.id.rb_safety_measures_sm4
binding!!.tvSignKey.text = getString(R.string.sm4_key)
}
else -> {
binding!!.tvSignKey.visibility = View.GONE
binding!!.etSignKey.visibility = View.GONE
}
}
binding!!.rgSafetyMeasures.check(safetyMeasuresId)
} else {
binding!!.etSignKey.setText(serverHistory[text])
}
}
true // allow selection
}.positiveText(R.string.select).negativeText(R.string.cancel).neutralText(R.string.clear_history).neutralColor(ResUtils.getColors(R.color.red)).onNeutral { _: MaterialDialog?, _: DialogAction? ->
serverHistory.clear()
HttpServerUtils.serverHistory = ""
}.show()
}
R.id.btn_server_test -> {
if (!CommonUtils.checkUrl(HttpServerUtils.serverAddress)) {
XToastUtils.error(getString(R.string.invalid_service_address))
return
}
queryConfig(true)
}
else -> {}
}
}
override fun onItemClick(itemView: View, item: PageInfo, position: Int) {
try {
if (item.name != ResUtils.getString(R.string.api_clone) && !CommonUtils.checkUrl(HttpServerUtils.serverAddress)) {
XToastUtils.error(getString(R.string.invalid_service_address))
serverConfig = null
return
}
if (serverConfig == null && item.name != ResUtils.getString(R.string.api_clone)) {
XToastUtils.error(getString(R.string.click_test_button_first))
return
}
if (serverConfig != null && ((item.name == ResUtils.getString(R.string.api_sms_send) && !serverConfig!!.enableApiSmsSend) || (item.name == ResUtils.getString(R.string.api_sms_query) && !serverConfig!!.enableApiSmsQuery) || (item.name == ResUtils.getString(R.string.api_call_query) && !serverConfig!!.enableApiCallQuery) || (item.name == ResUtils.getString(R.string.api_contact_query) && !serverConfig!!.enableApiContactQuery) || (item.name == ResUtils.getString(R.string.api_contact_add) && !serverConfig!!.enableApiContactAdd) || (item.name == ResUtils.getString(R.string.api_battery_query) && !serverConfig!!.enableApiBatteryQuery) || (item.name == ResUtils.getString(R.string.api_wol) && !serverConfig!!.enableApiWol) || (item.name == ResUtils.getString(R.string.api_location) && !serverConfig!!.enableApiLocation))) {
XToastUtils.error(getString(R.string.disabled_on_the_server))
return
}
@Suppress("UNCHECKED_CAST")
PageOption.to(Class.forName(item.classPath) as Class<XPageFragment>).setNewActivity(true).open(this)
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(e.message.toString())
}
}
private fun queryConfig(needToast: Boolean) {
val requestUrl: String = HttpServerUtils.serverAddress + "/config/query"
Log.i(TAG, "requestUrl:$requestUrl")
val msgMap: MutableMap<String, Any> = mutableMapOf()
val timestamp = System.currentTimeMillis()
msgMap["timestamp"] = timestamp
val clientSignKey = HttpServerUtils.clientSignKey
if (HttpServerUtils.clientSafetyMeasures != 0 && TextUtils.isEmpty(clientSignKey)) {
if (needToast) XToastUtils.error("请输入签名密钥/RSA公钥/SM4密钥")
return
}
if (HttpServerUtils.clientSafetyMeasures == 1 && !TextUtils.isEmpty(clientSignKey)) {
msgMap["sign"] = HttpServerUtils.calcSign(timestamp.toString(), clientSignKey)
}
val dataMap: MutableMap<String, Any> = mutableMapOf()
msgMap["data"] = dataMap
var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg")
val postRequest = XHttp.post(requestUrl).keepJson(true).timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE).timeStamp(true)
when (HttpServerUtils.clientSafetyMeasures) {
2 -> {
try {
val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey)
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
}
3 -> {
try {
val sm4Key = ConvertTools.hexStringToByteArray(HttpServerUtils.clientSignKey)
//requestMsg = Base64.encode(requestMsg.toByteArray())
val encryptCBC = SM4Crypt.encrypt(requestMsg.toByteArray(), sm4Key)
requestMsg = ConvertTools.bytes2HexString(encryptCBC)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
}
else -> {
postRequest.upJson(requestMsg)
}
}
if (needToast) mCountDownHelper?.start()
postRequest.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) {
XToastUtils.error(e.displayMessage)
if (needToast) mCountDownHelper?.finish()
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
try {
var json = response
if (HttpServerUtils.clientSafetyMeasures == 2) {
val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey)
json = RSACrypt.decryptByPublicKey(json, publicKey)
json = String(Base64.decode(json))
} else if (HttpServerUtils.clientSafetyMeasures == 3) {
val sm4Key = ConvertTools.hexStringToByteArray(HttpServerUtils.clientSignKey)
val encryptCBC = ConvertTools.hexStringToByteArray(json)
val decryptCBC = SM4Crypt.decrypt(encryptCBC, sm4Key)
json = String(decryptCBC)
}
val resp: BaseResponse<ConfigData> = Gson().fromJson(json, object : TypeToken<BaseResponse<ConfigData>>() {}.type)
if (resp.code == 200) {
serverConfig = resp.data!!
if (needToast) XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
//删除3.0.8之前保存的记录
serverHistory.remove(HttpServerUtils.serverAddress)
//添加到历史记录
val key = "${serverConfig?.extraDeviceMark}${HttpServerUtils.serverAddress}"
if (TextUtils.isEmpty(HttpServerUtils.clientSignKey)) {
serverHistory[key] = "SMSFORWARDER##" + HttpServerUtils.clientSafetyMeasures.toString()
} else {
serverHistory[key] = HttpServerUtils.clientSignKey + "##" + HttpServerUtils.clientSafetyMeasures.toString()
}
HttpServerUtils.serverHistory = Gson().toJson(serverHistory)
HttpServerUtils.serverConfig = Gson().toJson(serverConfig)
} else {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
if (needToast) mCountDownHelper?.finish()
} catch (e: Exception) {
e.printStackTrace()
if (needToast) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
mCountDownHelper?.finish()
}
}
}
})
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
}