新增:`主动控制`增加`远程WOL`功能(用于远程唤醒同一个局域网其他设备) #190
parent
2ca88ae495
commit
c53c3de118
@ -0,0 +1,181 @@
|
||||
package com.idormy.sms.forwarder.fragment.client
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.idormy.sms.forwarder.R
|
||||
import com.idormy.sms.forwarder.core.BaseFragment
|
||||
import com.idormy.sms.forwarder.databinding.FragmentClientWolSendBinding
|
||||
import com.idormy.sms.forwarder.server.model.BaseResponse
|
||||
import com.idormy.sms.forwarder.utils.HttpServerUtils
|
||||
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.xrouter.utils.TextUtils
|
||||
import com.xuexiang.xui.utils.CountDownButtonHelper
|
||||
import com.xuexiang.xui.utils.ResUtils
|
||||
import com.xuexiang.xui.widget.actionbar.TitleBar
|
||||
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
|
||||
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@Page(name = "远程WOL")
|
||||
class WolSendFragment : BaseFragment<FragmentClientWolSendBinding?>(), View.OnClickListener {
|
||||
|
||||
val TAG: String = WolSendFragment::class.java.simpleName
|
||||
private var mCountDownHelper: CountDownButtonHelper? = null
|
||||
private var wolHistory: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
override fun viewBindingInflate(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup,
|
||||
): FragmentClientWolSendBinding {
|
||||
return FragmentClientWolSendBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun initTitle(): TitleBar? {
|
||||
return super.initTitle()!!.setImmersive(false).setTitle(R.string.api_wol)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化控件
|
||||
*/
|
||||
override fun initViews() {
|
||||
//发送按钮增加倒计时,避免重复点击
|
||||
mCountDownHelper = CountDownButtonHelper(binding!!.btnSubmit, SettingUtils.requestTimeout)
|
||||
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
|
||||
override fun onCountDown(time: Int) {
|
||||
binding!!.btnSubmit.text = String.format(getString(R.string.seconds_n), time)
|
||||
}
|
||||
|
||||
override fun onFinished() {
|
||||
binding!!.btnSubmit.text = getString(R.string.send)
|
||||
}
|
||||
})
|
||||
|
||||
//取出历史记录
|
||||
val history = HttpServerUtils.wolHistory
|
||||
if (!TextUtils.isEmpty(history)) {
|
||||
wolHistory = Gson().fromJson(history, object : TypeToken<MutableMap<String, String>>() {}.type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initListeners() {
|
||||
binding!!.btnServerHistory.setOnClickListener(this)
|
||||
binding!!.btnSubmit.setOnClickListener(this)
|
||||
}
|
||||
|
||||
@SingleClick
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.btn_server_history -> {
|
||||
if (wolHistory.isEmpty()) {
|
||||
XToastUtils.warning(getString(R.string.no_server_history))
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "wolHistory = $wolHistory")
|
||||
|
||||
MaterialDialog.Builder(context!!)
|
||||
.title(R.string.server_history)
|
||||
.items(wolHistory.keys)
|
||||
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
|
||||
//XToastUtils.info("$which: $text")
|
||||
binding!!.etIp.setText(text)
|
||||
binding!!.etMac.setText(wolHistory[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? ->
|
||||
wolHistory.clear()
|
||||
HttpServerUtils.wolHistory = ""
|
||||
}
|
||||
.show()
|
||||
}
|
||||
R.id.btn_submit -> {
|
||||
val requestUrl: String = HttpServerUtils.serverAddress + "/wol/send"
|
||||
Log.i(TAG, "requestUrl:$requestUrl")
|
||||
|
||||
val msgMap: MutableMap<String, Any> = mutableMapOf()
|
||||
val timestamp = System.currentTimeMillis()
|
||||
msgMap["timestamp"] = timestamp
|
||||
val clientSignKey = HttpServerUtils.clientSignKey
|
||||
if (!TextUtils.isEmpty(clientSignKey)) {
|
||||
msgMap["sign"] = HttpServerUtils.calcSign(timestamp.toString(), clientSignKey.toString())
|
||||
}
|
||||
|
||||
val ip = binding!!.etIp.text.toString()
|
||||
val ipRegex = getString(R.string.ip_regex).toRegex()
|
||||
if (!ipRegex.matches(ip)) {
|
||||
XToastUtils.error(ResUtils.getString(R.string.ip_error))
|
||||
return
|
||||
}
|
||||
|
||||
val mac = binding!!.etMac.text.toString()
|
||||
val macRegex = getString(R.string.mac_regex).toRegex()
|
||||
if (!macRegex.matches(mac)) {
|
||||
XToastUtils.error(ResUtils.getString(R.string.mac_error))
|
||||
return
|
||||
}
|
||||
|
||||
val dataMap: MutableMap<String, Any> = mutableMapOf()
|
||||
dataMap["ip"] = ip
|
||||
dataMap["mac"] = mac
|
||||
msgMap["data"] = dataMap
|
||||
|
||||
val requestMsg: String = Gson().toJson(msgMap)
|
||||
Log.i(TAG, "requestMsg:$requestMsg")
|
||||
|
||||
mCountDownHelper?.start()
|
||||
XHttp.post(requestUrl)
|
||||
.upJson(requestMsg)
|
||||
.keepJson(true)
|
||||
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
|
||||
.cacheMode(CacheMode.NO_CACHE)
|
||||
.timeStamp(true)
|
||||
.execute(object : SimpleCallBack<String>() {
|
||||
|
||||
override fun onError(e: ApiException) {
|
||||
XToastUtils.error(e.displayMessage)
|
||||
}
|
||||
|
||||
override fun onSuccess(response: String) {
|
||||
Log.i(TAG, response)
|
||||
try {
|
||||
val resp: BaseResponse<String> = Gson().fromJson(response, object : TypeToken<BaseResponse<String>>() {}.type)
|
||||
if (resp.code == 200) {
|
||||
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
|
||||
//添加到历史记录
|
||||
wolHistory[ip] = mac
|
||||
HttpServerUtils.wolHistory = Gson().toJson(wolHistory)
|
||||
} else {
|
||||
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.idormy.sms.forwarder.server.controller
|
||||
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.util.Log
|
||||
import com.idormy.sms.forwarder.server.model.BaseRequest
|
||||
import com.idormy.sms.forwarder.server.model.WolData
|
||||
import com.yanzhenjie.andserver.annotation.*
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
|
||||
@Suppress("PrivatePropertyName")
|
||||
@RestController
|
||||
@RequestMapping(path = ["/wol"])
|
||||
class WolController {
|
||||
|
||||
private val TAG: String = WolController::class.java.simpleName
|
||||
|
||||
//远程WOL
|
||||
@CrossOrigin(methods = [RequestMethod.POST])
|
||||
@PostMapping("/send")
|
||||
fun send(@RequestBody bean: BaseRequest<WolData>): String {
|
||||
val wolData = bean.data
|
||||
Log.d(TAG, wolData.toString())
|
||||
|
||||
val policy = ThreadPolicy.Builder().permitAll().build()
|
||||
StrictMode.setThreadPolicy(policy)
|
||||
DatagramSocket().use { socket ->
|
||||
try {
|
||||
val macBytes = getMacBytes(wolData.mac)
|
||||
val bytes = ByteArray(6 + 16 * macBytes.size)
|
||||
for (i in 0..5) {
|
||||
bytes[i] = 0xff.toByte()
|
||||
}
|
||||
var i = 6
|
||||
while (i < bytes.size) {
|
||||
System.arraycopy(macBytes, 0, bytes, i, macBytes.size)
|
||||
i += macBytes.size
|
||||
}
|
||||
val address: InetAddress = InetAddress.getByName(wolData.ip)
|
||||
val packet = DatagramPacket(bytes, bytes.size, address, 9)
|
||||
socket.send(packet)
|
||||
Log.d(TAG, "Wake-on-LAN packet sent.")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send Wake-on-LAN packet: $e")
|
||||
}
|
||||
}
|
||||
|
||||
return "success"
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
private fun getMacBytes(macStr: String): ByteArray {
|
||||
val bytes = ByteArray(6)
|
||||
val hex = macStr.replace("-", ":").split(":").toTypedArray()
|
||||
require(hex.size == 6) { "Invalid MAC address." }
|
||||
try {
|
||||
for (i in 0..5) {
|
||||
bytes[i] = hex[i].toInt(16).toByte()
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException("Invalid hex digit in MAC address. $e")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.idormy.sms.forwarder.server.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.io.Serializable
|
||||
|
||||
data class WolData(
|
||||
@SerializedName("ip")
|
||||
var ip: String,
|
||||
@SerializedName("mac")
|
||||
var mac: String,
|
||||
) : Serializable
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,108 @@
|
||||
<?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:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:contentDescription="@string/api_wol"
|
||||
app:srcCompat="@drawable/icon_api_wol" />
|
||||
|
||||
<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/ip" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_ip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/ip_hint"
|
||||
android:singleLine="true"
|
||||
app:met_clearButton="true"
|
||||
app:met_errorMessage="@string/ip_error"
|
||||
app:met_regexp="@string/ip_regex"
|
||||
app:met_validateOnFocusLost="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/mac" />
|
||||
|
||||
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
|
||||
android:id="@+id/et_mac"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/mac_hint"
|
||||
android:singleLine="true"
|
||||
app:met_clearButton="true"
|
||||
app:met_errorMessage="@string/mac_error"
|
||||
app:met_regexp="@string/mac_regex"
|
||||
app:met_validateOnFocusLost="true" />
|
||||
|
||||
</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_server_history"
|
||||
style="@style/SuperButton.Gray.Icon"
|
||||
android:drawableStart="@drawable/ic_restore"
|
||||
android:paddingStart="7dp"
|
||||
android:text="@string/server_history"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
|
||||
android:id="@+id/btn_submit"
|
||||
style="@style/SuperButton.Blue.Icon"
|
||||
android:layout_marginStart="20dp"
|
||||
android:drawableStart="@drawable/ic_send_white"
|
||||
android:paddingStart="20dp"
|
||||
android:text="@string/send"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
Loading…
Reference in New Issue