新增:主动控制增加远程WOL功能(用于远程唤醒同一个局域网其他设备) #190

This commit is contained in:
pppscn 2022-07-28 16:04:22 +08:00
parent 2ca88ae495
commit c53c3de118
13 changed files with 451 additions and 2 deletions

1
.gitignore vendored
View File

@ -34,3 +34,4 @@
/app/mapping.txt
/app/seeds.txt
/app/unused.txt
/pic/*.bkp

View File

@ -119,6 +119,11 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
HttpServerUtils.enableApiBatteryQuery = isChecked
}
binding!!.sbApiWol.isChecked = HttpServerUtils.enableApiWol
binding!!.sbApiWol.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
HttpServerUtils.enableApiWol = isChecked
}
}
@SingleClick

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -240,6 +240,8 @@ const val SP_ENABLE_API_SMS_QUERY = "enable_api_sms_query"
const val SP_ENABLE_API_CALL_QUERY = "enable_api_call_query"
const val SP_ENABLE_API_CONTACT_QUERY = "enable_api_contact_query"
const val SP_ENABLE_API_BATTERY_QUERY = "enable_api_battery_query"
const val SP_ENABLE_API_WOL = "enable_api_wol"
const val SP_WOL_HISTORY = "wol_history"
const val SP_SERVER_ADDRESS = "server_address"
const val SP_SERVER_HISTORY = "server_history"
const val SP_CLIENT_SIGN_KEY = "client_sign_key"
@ -250,4 +252,5 @@ var CLIENT_FRAGMENT_LIST = listOf(
PageInfo(getString(R.string.api_call_query), "com.idormy.sms.forwarder.fragment.client.CallQueryFragment", "{\"\":\"\"}", CoreAnim.slide, R.drawable.icon_api_call_query),
PageInfo(getString(R.string.api_contact_query), "com.idormy.sms.forwarder.fragment.client.ContactQueryFragment", "{\"\":\"\"}", CoreAnim.slide, R.drawable.icon_api_contact_query),
PageInfo(getString(R.string.api_battery_query), "com.idormy.sms.forwarder.fragment.client.BatteryQueryFragment", "{\"\":\"\"}", CoreAnim.slide, R.drawable.icon_api_battery_query),
PageInfo(getString(R.string.api_wol), "com.idormy.sms.forwarder.fragment.client.WolSendFragment", "{\"\":\"\"}", CoreAnim.slide, R.drawable.icon_api_wol),
)

View File

@ -112,6 +112,22 @@ class HttpServerUtils private constructor() {
MMKVUtils.put(SP_ENABLE_API_BATTERY_QUERY, enableApiQueryBattery)
}
//是否启用远程WOL
@JvmStatic
var enableApiWol: Boolean
get() = MMKVUtils.getBoolean(SP_ENABLE_API_WOL, false)
set(enableApiWol) {
MMKVUtils.put(SP_ENABLE_API_WOL, enableApiWol)
}
//WOL历史记录
@JvmStatic
var wolHistory: String?
get() = MMKVUtils.getString(SP_WOL_HISTORY, "")
set(wolHistory) {
MMKVUtils.put(SP_WOL_HISTORY, wolHistory)
}
//计算签名
fun calcSign(timestamp: String, signSecret: String): String {
val stringToSign = "$timestamp\n" + signSecret

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -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>

View File

@ -415,6 +415,41 @@
</LinearLayout>
<LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/api_wol"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/api_wol_tips"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_api_wol"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -781,6 +781,8 @@
<string name="api_contact_query_tips">Remotely check contact list</string>
<string name="api_battery_query">Query Battery</string>
<string name="api_battery_query_tips">Remotely query mobile phone power and battery status</string>
<string name="api_wol">Remotely WOL</string>
<string name="api_wol_tips">Turn on your Wake-On-LAN enabled devices remotely</string>
<string name="sim_slot">Sim Slot</string>
<string name="phone_numbers">Phone Numbers</string>
@ -844,8 +846,8 @@
<string name="over_level_max">[Battery Warning] The battery warning limit has been exceeded, please unplug the charger!%s</string>
<string name="reach_level_min">[Battery Warning] The lower limit of the battery warning has been reached, please charge it in time!%s</string>
<string name="reach_level_max">[Battery Warning] The upper limit of battery warning has been reached, please unplug the charger!%s</string>
<string name="battery_status_changed">【充电状态】发生变化:</string>
<string name="no_indentation_allowed_on_the_first_line">第一行不允许缩进</string>
<string name="battery_status_changed">[Charging status] changes:</string>
<string name="no_indentation_allowed_on_the_first_line">No indentation allowed on the first line</string>
<string name="sign_required">The server enables the signing key, and the sign node required</string>
<string name="timestamp_required">The server enables the signing key, and the timestamp node required</string>
<string name="sign_verify_failed">Sign verify failed</string>
@ -881,4 +883,13 @@
<string name="appsecret">AppSecret</string>
<string name="sampleText">Sample Text</string>
<string name="sampleMarkdown">Sample Markdown</string>
<string name="ip_hint">Please enter an IP address, eg. 192.168.168.168</string>
<string name="ip_error">Malformed IP address, eg. 192.168.168.168</string>
<string name="ip_regex">^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$</string>
<string name="mac_hint">Please enter the network card mac, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">The network card mac format is incorrect, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="ip">IP</string>
<string name="mac">MAC</string>
<string name="no_wol_history">There is no history record, WOL will be added automatically after successful sending</string>
</resources>

View File

@ -782,6 +782,8 @@
<string name="api_contact_query_tips">远程查联系人列表</string>
<string name="api_battery_query">远程查电量</string>
<string name="api_battery_query_tips">远程查询手机电量与电池状态</string>
<string name="api_wol">远程WOL</string>
<string name="api_wol_tips">远程打开启用LAN唤醒功能(Wake-On-LAN)的设备</string>
<string name="sim_slot">发送卡槽</string>
<string name="phone_numbers">手机号码</string>
@ -882,4 +884,13 @@
<string name="appsecret">AppSecret</string>
<string name="sampleText">文本类型</string>
<string name="sampleMarkdown">Markdown类型</string>
<string name="ip_hint">请输入IP地址例如192.168.168.168</string>
<string name="ip_error">IP地址格式错误例如192.168.168.168</string>
<string name="ip_regex">^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$</string>
<string name="mac_hint">请输入网卡mac例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">网卡mac格式错误例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="ip">IP地址</string>
<string name="mac">网卡MAC</string>
<string name="no_wol_history">暂无历史记录WOL发送成功后自动加入</string>
</resources>