Compare commits
20 Commits
493501b809
...
backup-bef
| Author | SHA1 | Date | |
|---|---|---|---|
| 57cf264e44 | |||
|
|
9bfb1a9040 | ||
| cad16fa7ca | |||
| 2ae2ab0ff5 | |||
| ccc0c3c4ae | |||
| 9a9ffb79d5 | |||
| 1d9a77a112 | |||
| 8efa4889be | |||
| 81615a27d9 | |||
| 473e2c2d89 | |||
| c6fbcec2d6 | |||
| 22e6aff8c6 | |||
| 6a70f7ef5d | |||
| 518a7d813e | |||
| 701c5a815f | |||
| e2160aa59f | |||
| f195ef614d | |||
| 287a1ece7f | |||
| 79ed03c0fd | |||
| 1a012937cf |
@@ -14,7 +14,7 @@ object Constant {
|
|||||||
var LONG_INTERVAL = BASE_LONG_INTERVAL
|
var LONG_INTERVAL = BASE_LONG_INTERVAL
|
||||||
var CHANGE_PAGE_INTERVAL = BASE_CHANGE_PAGE_INTERVAL
|
var CHANGE_PAGE_INTERVAL = BASE_CHANGE_PAGE_INTERVAL
|
||||||
var POP_WINDOW_INTERVAL = BASE_POP_WINDOW_INTERVAL
|
var POP_WINDOW_INTERVAL = BASE_POP_WINDOW_INTERVAL
|
||||||
private const val DEFAULT_HOST = "ws://192.168.6.50:8080"
|
private const val DEFAULT_HOST = "ws://8.166.130.74:18680"
|
||||||
|
|
||||||
var version = Int.MAX_VALUE
|
var version = Int.MAX_VALUE
|
||||||
var myName = ""
|
var myName = ""
|
||||||
@@ -58,7 +58,7 @@ object Constant {
|
|||||||
SPUtils.getInstance().put(weworkCorpName + "weworkMP", value)
|
SPUtils.getInstance().put(weworkCorpName + "weworkMP", value)
|
||||||
}
|
}
|
||||||
var encryptType: Int = SPUtils.getInstance().getInt("encryptType", 0)
|
var encryptType: Int = SPUtils.getInstance().getInt("encryptType", 0)
|
||||||
var autoReply: Int = SPUtils.getInstance().getInt("autoReply", 1)
|
var autoReply: Int = SPUtils.getInstance().getInt("autoReply", 0)
|
||||||
var groupStrict: Boolean
|
var groupStrict: Boolean
|
||||||
get() = SPUtils.getInstance().getBoolean("groupStrict", false)
|
get() = SPUtils.getInstance().getBoolean("groupStrict", false)
|
||||||
set(value) = SPUtils.getInstance().put("groupStrict", value)
|
set(value) = SPUtils.getInstance().put("groupStrict", value)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.yameida.worktool
|
|
||||||
|
|
||||||
import com.blankj.utilcode.util.TimeUtils
|
|
||||||
import org.yameida.worktool.model.WeworkMessageBean
|
|
||||||
import org.yameida.worktool.service.MyLooper
|
|
||||||
import org.yameida.worktool.service.WeworkController
|
|
||||||
import org.yameida.worktool.service.WeworkLoopImpl
|
|
||||||
import org.yameida.worktool.service.getRoot
|
|
||||||
import org.yameida.worktool.utils.AccessibilityUtil
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 示例
|
|
||||||
*/
|
|
||||||
object Demo {
|
|
||||||
|
|
||||||
fun test(flag: Boolean) {
|
|
||||||
if (!flag) return
|
|
||||||
|
|
||||||
MyLooper.getInstance().removeCallbacksAndMessages(null)
|
|
||||||
|
|
||||||
//打印当前视图树
|
|
||||||
// AccessibilityUtil.printNodeClazzTree(getRoot())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun test2(name: String) {
|
|
||||||
val time = TimeUtils.date2String(Date(), "MMddHHmm")
|
|
||||||
val groupName = "测试群$time"
|
|
||||||
val json = """
|
|
||||||
{
|
|
||||||
"socketType":2,
|
|
||||||
"list":[
|
|
||||||
{
|
|
||||||
"type":203,
|
|
||||||
"titleList":[
|
|
||||||
"$name"
|
|
||||||
],
|
|
||||||
"receivedContent":"你好~我是机器人,你可以@我和我聊天,你也可以通过API文档来让我发送消息或完成建群等任务。接口文档:https://www.apifox.cn/apidoc/project-1035094/api-23520034"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 206,
|
|
||||||
"groupName": "$groupName",
|
|
||||||
"selectList": [
|
|
||||||
"$name",
|
|
||||||
"甲仑"
|
|
||||||
],
|
|
||||||
"groupAnnouncement": "(自动拉群+自动群公告) WorkTool欢迎大家~WorkTool管家是机器人,有问题可以在QQ群反馈~@我可以聊天~"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
MyLooper.onMessage(null, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,11 +22,13 @@ import org.yameida.worktool.utils.*
|
|||||||
import org.yameida.worktool.utils.capture.MediaProjectionHolder
|
import org.yameida.worktool.utils.capture.MediaProjectionHolder
|
||||||
import org.yameida.worktool.utils.envcheck.CheckHook
|
import org.yameida.worktool.utils.envcheck.CheckHook
|
||||||
import org.yameida.worktool.utils.envcheck.CheckRoot
|
import org.yameida.worktool.utils.envcheck.CheckRoot
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
class ListenActivity : AppCompatActivity() {
|
class ListenActivity : AppCompatActivity() {
|
||||||
|
|
||||||
var riskRetry: Int = 0
|
var riskRetry: Int = 0
|
||||||
|
private var updatingAccessibilitySwitch = false
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@@ -59,12 +61,13 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
unregisterReceiver(openWsReceiver)
|
unregisterReceiver(openWsReceiver)
|
||||||
|
FloatWindowHelper.hideWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
sw_overlay.isChecked = Settings.canDrawOverlays(Utils.getApp()) && FlowPermissionHelper.canBackgroundStart(Utils.getApp())
|
sw_overlay.isChecked = Settings.canDrawOverlays(Utils.getApp()) && FlowPermissionHelper.canBackgroundStart(Utils.getApp())
|
||||||
sw_accessibility.isChecked = PermissionHelper.isAccessibilitySettingOn()
|
refreshAccessibilitySwitch()
|
||||||
if (needToWork) {
|
if (needToWork) {
|
||||||
needToWork = false
|
needToWork = false
|
||||||
goToWork()
|
goToWork()
|
||||||
@@ -75,16 +78,11 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
iv_settings.setOnClickListener {
|
iv_settings.setOnClickListener {
|
||||||
SettingsActivity.enterActivity(this)
|
SettingsActivity.enterActivity(this)
|
||||||
}
|
}
|
||||||
et_channel.setText(Constant.robotId)
|
tv_channel.text = Constant.robotId.ifBlank { "未生成" }
|
||||||
bt_save.setOnClickListener {
|
bt_reset_channel.setOnClickListener {
|
||||||
val channel = et_channel.text.toString().trim()
|
val channel = generateDefaultRobotId()
|
||||||
Constant.robotId = channel
|
tv_channel.text = channel
|
||||||
ToastUtils.showLong("保存成功")
|
saveChannel(channel, toast = "重置成功")
|
||||||
sendBroadcast(Intent(Constant.WEWORK_NOTIFY).apply {
|
|
||||||
putExtra("type", "modify_channel")
|
|
||||||
})
|
|
||||||
HttpUtil.getMyConfig(toast = false)
|
|
||||||
MobclickAgent.onProfileSignIn(channel)
|
|
||||||
}
|
}
|
||||||
tv_host.text = Constant.host
|
tv_host.text = Constant.host
|
||||||
tv_host.setOnClickListener {
|
tv_host.setOnClickListener {
|
||||||
@@ -131,6 +129,9 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun initAccessibility() {
|
private fun initAccessibility() {
|
||||||
sw_accessibility.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
|
sw_accessibility.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
|
||||||
|
if (updatingAccessibilitySwitch) {
|
||||||
|
return@OnCheckedChangeListener
|
||||||
|
}
|
||||||
LogUtils.i("sw_accessibility onCheckedChanged: $isChecked")
|
LogUtils.i("sw_accessibility onCheckedChanged: $isChecked")
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
if (Constant.robotId.isBlank()) {
|
if (Constant.robotId.isBlank()) {
|
||||||
@@ -185,13 +186,45 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initData() {
|
private fun initData() {
|
||||||
Constant.robotId = "111"
|
// 链接号为空时自动生成一次并持久化,避免每次启动覆盖用户手动保存的链接号
|
||||||
Constant.host = "ws://192.168.6.50:8080"
|
if (Constant.robotId.isBlank()) {
|
||||||
|
Constant.robotId = generateDefaultRobotId()
|
||||||
|
tv_channel.text = Constant.robotId
|
||||||
|
}
|
||||||
// HttpUtil.checkUpdate()
|
// HttpUtil.checkUpdate()
|
||||||
HttpUtil.getMyConfig(toast = false)
|
HttpUtil.getMyConfig(toast = false)
|
||||||
CacheUtil.autoDelete()
|
CacheUtil.autoDelete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateDefaultRobotId(): String {
|
||||||
|
val suffix = Random.nextInt(1000, 9999)
|
||||||
|
return "${System.currentTimeMillis()}$suffix"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveChannel(channel: String, toast: String) {
|
||||||
|
if (channel.isBlank()) {
|
||||||
|
ToastUtils.showLong("链接号不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Constant.robotId = channel
|
||||||
|
ToastUtils.showLong(toast)
|
||||||
|
sendBroadcast(Intent(Constant.WEWORK_NOTIFY).apply {
|
||||||
|
putExtra("type", "modify_channel")
|
||||||
|
})
|
||||||
|
if (WeworkController.isServiceReady()) {
|
||||||
|
runCatching {
|
||||||
|
WeworkController.weworkService.reconnectWebSocket("modify_channel_direct")
|
||||||
|
}.onFailure {
|
||||||
|
LogUtils.w("重置后直连重连失败,等待广播触发: ${it.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastUtils.showLong("无障碍服务未运行,请先开启后再重置")
|
||||||
|
LogUtils.w("重置时服务未就绪,无法触发直连重连")
|
||||||
|
}
|
||||||
|
HttpUtil.getMyConfig(toast = false)
|
||||||
|
MobclickAgent.onProfileSignIn(channel)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initNotification() {
|
private fun initNotification() {
|
||||||
if (!Constant.enableMediaProject) {
|
if (!Constant.enableMediaProject) {
|
||||||
return
|
return
|
||||||
@@ -206,7 +239,7 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
}, Context.BIND_AUTO_CREATE)
|
}, Context.BIND_AUTO_CREATE)
|
||||||
//开启屏幕录制权限
|
//开启屏幕录制权限
|
||||||
if (MediaProjectionHolder.mMediaProjection == null) {
|
if (MediaProjectionHolder.mMediaProjection == null) {
|
||||||
bt_save.postDelayed({
|
window.decorView.postDelayed({
|
||||||
fastStartActivity(this, GetScreenShotActivity::class.java)
|
fastStartActivity(this, GetScreenShotActivity::class.java)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -282,8 +315,8 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
.setNegativeButton("", null)
|
.setNegativeButton("", null)
|
||||||
.setPositiveButton("", null)
|
.setPositiveButton("", null)
|
||||||
val show = positiveButton.show()
|
val show = positiveButton.show()
|
||||||
bt_save.postDelayed({ show.dismiss() }, 5000)
|
window.decorView.postDelayed({ show.dismiss() }, 5000)
|
||||||
bt_save.postDelayed({
|
window.decorView.postDelayed({
|
||||||
packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
|
packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
|
||||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(this)
|
startActivity(this)
|
||||||
@@ -293,10 +326,21 @@ class ListenActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val openWsReceiver = object : BroadcastReceiver() {
|
private val openWsReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.getStringExtra("type") == "openWs") {
|
when (intent.getStringExtra("type")) {
|
||||||
|
"openWs" -> {
|
||||||
needToWork = intent.getBooleanExtra("switch", false)
|
needToWork = intent.getBooleanExtra("switch", false)
|
||||||
}
|
}
|
||||||
|
"accessibility_state" -> {
|
||||||
|
refreshAccessibilitySwitch(intent.getBooleanExtra("enabled", PermissionHelper.isAccessibilitySettingOn()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshAccessibilitySwitch(checked: Boolean = PermissionHelper.isAccessibilitySettingOn()) {
|
||||||
|
updatingAccessibilitySwitch = true
|
||||||
|
sw_accessibility.isChecked = checked
|
||||||
|
updatingAccessibilitySwitch = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
})
|
})
|
||||||
rl_reply_strategy.setOnClickListener { showReplyStrategyDialog() }
|
rl_reply_strategy.setOnClickListener { showReplyStrategyDialog() }
|
||||||
rl_log.setOnClickListener { showLogDialog() }
|
rl_log.setOnClickListener { showLogDialog() }
|
||||||
rl_update.setOnClickListener { showUpdateDialog() }
|
|
||||||
rl_donate.setOnClickListener { showDonateDialog() }
|
|
||||||
rl_share.setOnClickListener { showShareDialog() }
|
|
||||||
rl_advance.setOnClickListener { SettingsAdvanceActivity.enterActivity(this) }
|
rl_advance.setOnClickListener { SettingsAdvanceActivity.enterActivity(this) }
|
||||||
freshOpenFlow()
|
freshOpenFlow()
|
||||||
bt_open_flow.setOnClickListener {
|
bt_open_flow.setOnClickListener {
|
||||||
@@ -148,37 +145,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUpdateDialog() {
|
|
||||||
if (Constant.getMasterCheckUpdateUrl() == Constant.getCheckUpdateUrl()) {
|
|
||||||
HttpUtil.checkUpdate()
|
|
||||||
} else {
|
|
||||||
QMUIDialog.CheckableDialogBuilder(this)
|
|
||||||
.setTitle("检查新版本")
|
|
||||||
.addItems(arrayOf("检查当前Host新版本", "检查${getString(R.string.app_name)}官方新版本")) { dialog, which ->
|
|
||||||
dialog.dismiss()
|
|
||||||
if (which == 0) {
|
|
||||||
HttpUtil.checkUpdate()
|
|
||||||
} else {
|
|
||||||
HttpUtil.checkUpdate(Constant.getMasterCheckUpdateUrl())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.create(R.style.QMUI_Dialog)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showDonateDialog() {
|
|
||||||
DonateUtil.zfbDonate(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showShareDialog() {
|
|
||||||
startActivity(Intent.createChooser(Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
type = ShareUtil.TEXT
|
|
||||||
putExtra(Intent.EXTRA_TEXT, "我发现一个非常好用的企业微信机器人程序,文档地址: https://worktool.apifox.cn/ APP下载地址是: https://cdn.asrtts.cn/uploads/worktool/apk/worktool-latest.apk")
|
|
||||||
}, "分享"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun freshOpenFlow() {
|
private fun freshOpenFlow() {
|
||||||
if (Settings.canDrawOverlays(Utils.getApp())) {
|
if (Settings.canDrawOverlays(Utils.getApp())) {
|
||||||
if (FlowPermissionHelper.canBackgroundStart(Utils.getApp())) {
|
if (FlowPermissionHelper.canBackgroundStart(Utils.getApp())) {
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ class SettingsAdvanceActivity : AppCompatActivity() {
|
|||||||
ll_corp_param.visibility = if (Constant.customLink) View.VISIBLE else View.GONE
|
ll_corp_param.visibility = if (Constant.customLink) View.VISIBLE else View.GONE
|
||||||
rl_username.visibility = if (Constant.customMP) View.VISIBLE else View.GONE
|
rl_username.visibility = if (Constant.customMP) View.VISIBLE else View.GONE
|
||||||
rl_qa_url.setOnClickListener { showQaUrlDialog() }
|
rl_qa_url.setOnClickListener { showQaUrlDialog() }
|
||||||
|
rl_host.setOnClickListener { showHostDialog() }
|
||||||
|
tv_select_host.text = Constant.host
|
||||||
rl_corp_name.setOnClickListener { showCorpNameDialog() }
|
rl_corp_name.setOnClickListener { showCorpNameDialog() }
|
||||||
rl_corp.setOnClickListener { showCorpIdDialog() }
|
rl_corp.setOnClickListener { showCorpIdDialog() }
|
||||||
rl_agent.setOnClickListener { showAgentIdDialog() }
|
rl_agent.setOnClickListener { showAgentIdDialog() }
|
||||||
@@ -117,6 +119,32 @@ class SettingsAdvanceActivity : AppCompatActivity() {
|
|||||||
.create(R.style.QMUI_Dialog).show()
|
.create(R.style.QMUI_Dialog).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showHostDialog() {
|
||||||
|
val builder = QMUIDialog.EditTextDialogBuilder(this)
|
||||||
|
builder.setTitle("服务器地址")
|
||||||
|
.setPlaceholder("请输入WebSocket服务器地址")
|
||||||
|
.setDefaultText(Constant.host)
|
||||||
|
.setInputType(InputType.TYPE_CLASS_TEXT)
|
||||||
|
.addAction(getString(R.string.cancel)) { dialog, index -> dialog.dismiss() }
|
||||||
|
.addAction(getString(R.string.add)) { dialog, index ->
|
||||||
|
val text = builder.editText.text
|
||||||
|
if (text != null && text.isNotEmpty()) {
|
||||||
|
val host = text.toString().trim()
|
||||||
|
if (host.matches("wss?://[^:]+:\\d+".toRegex())) {
|
||||||
|
dialog.dismiss()
|
||||||
|
Constant.host = host
|
||||||
|
tv_select_host.text = host
|
||||||
|
ToastUtils.showLong("服务器地址已更新")
|
||||||
|
} else {
|
||||||
|
ToastUtils.showLong("格式异常!示例: ws://192.168.1.1:18680")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastUtils.showLong("请勿为空!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create(R.style.QMUI_Dialog).show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateRobotQaUrl(callbackUrl: String) {
|
private fun updateRobotQaUrl(callbackUrl: String) {
|
||||||
try {
|
try {
|
||||||
val json = hashMapOf<String, Any>()
|
val json = hashMapOf<String, Any>()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.yameida.worktool.service
|
package org.yameida.worktool.service
|
||||||
|
|
||||||
import com.blankj.utilcode.util.*
|
import com.blankj.utilcode.util.*
|
||||||
import org.yameida.worktool.Demo
|
|
||||||
import org.yameida.worktool.annotation.RequestMapping
|
import org.yameida.worktool.annotation.RequestMapping
|
||||||
import org.yameida.worktool.model.ExecCallbackBean
|
import org.yameida.worktool.model.ExecCallbackBean
|
||||||
import org.yameida.worktool.model.WeworkMessageBean
|
import org.yameida.worktool.model.WeworkMessageBean
|
||||||
@@ -18,6 +17,8 @@ object WeworkController {
|
|||||||
/** 是否正在等待回复 - 等待时会暂停主循环扫描新消息 */
|
/** 是否正在等待回复 - 等待时会暂停主循环扫描新消息 */
|
||||||
var waitingForReply = false
|
var waitingForReply = false
|
||||||
|
|
||||||
|
fun isServiceReady(): Boolean = ::weworkService.isInitialized
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交互通知
|
* 交互通知
|
||||||
* @see WeworkMessageBean.TYPE_CONSOLE_TOAST
|
* @see WeworkMessageBean.TYPE_CONSOLE_TOAST
|
||||||
@@ -63,10 +64,15 @@ object WeworkController {
|
|||||||
@RequestMapping
|
@RequestMapping
|
||||||
fun sendMessage(message: WeworkMessageBean): Boolean {
|
fun sendMessage(message: WeworkMessageBean): Boolean {
|
||||||
LogUtils.d("REQUEST sendMessage(): ${message.messageId} ${message.titleList} ${message.receivedContent} ${message.at} ${message.atList?.joinToString()}")
|
LogUtils.d("REQUEST sendMessage(): ${message.messageId} ${message.titleList} ${message.receivedContent} ${message.at} ${message.atList?.joinToString()}")
|
||||||
|
// 进入发送消息流程,暂停主循环检测新消息,避免重复进入同一聊天页
|
||||||
|
waitingForReply = true
|
||||||
|
try {
|
||||||
val result = WeworkOperationImpl.sendMessage(message, message.titleList, message.receivedContent, message.at, message.atList)
|
val result = WeworkOperationImpl.sendMessage(message, message.titleList, message.receivedContent, message.at, message.atList)
|
||||||
// 发送完成,通知等待结束
|
|
||||||
waitingForReply = false
|
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
// 无论成功还是失败,都要恢复等待状态
|
||||||
|
waitingForReply = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +88,9 @@ object WeworkController {
|
|||||||
@RequestMapping
|
@RequestMapping
|
||||||
fun replyMessage(message: WeworkMessageBean): Boolean {
|
fun replyMessage(message: WeworkMessageBean): Boolean {
|
||||||
LogUtils.d("REQUEST replyMessage(): ${message.messageId} ${message.receivedName} ${message.originalContent} ${message.textType} ${message.receivedContent}")
|
LogUtils.d("REQUEST replyMessage(): ${message.messageId} ${message.receivedName} ${message.originalContent} ${message.textType} ${message.receivedContent}")
|
||||||
|
// 进入发送消息流程,暂停主循环检测新消息,避免重复进入同一聊天页
|
||||||
|
waitingForReply = true
|
||||||
|
try {
|
||||||
val result = WeworkOperationImpl.replyMessage(
|
val result = WeworkOperationImpl.replyMessage(
|
||||||
message,
|
message,
|
||||||
message.titleList,
|
message.titleList,
|
||||||
@@ -90,9 +99,11 @@ object WeworkController {
|
|||||||
message.textType,
|
message.textType,
|
||||||
message.receivedContent
|
message.receivedContent
|
||||||
)
|
)
|
||||||
// 发送完成,通知等待结束
|
|
||||||
waitingForReply = false
|
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
// 无论成功还是失败,都要恢复等待状态
|
||||||
|
waitingForReply = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在房间内转发消息
|
* 在房间内转发消息
|
||||||
@@ -149,7 +160,6 @@ object WeworkController {
|
|||||||
@RequestMapping
|
@RequestMapping
|
||||||
fun test(message: WeworkMessageBean? = null) {
|
fun test(message: WeworkMessageBean? = null) {
|
||||||
LogUtils.d(message)
|
LogUtils.d(message)
|
||||||
Demo.test(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.core.text.isDigitsOnly
|
|||||||
import com.blankj.utilcode.util.*
|
import com.blankj.utilcode.util.*
|
||||||
import com.hjq.toast.ToastUtils
|
import com.hjq.toast.ToastUtils
|
||||||
import org.yameida.worktool.Constant
|
import org.yameida.worktool.Constant
|
||||||
import org.yameida.worktool.Demo
|
|
||||||
import org.yameida.worktool.MyApplication
|
import org.yameida.worktool.MyApplication
|
||||||
import org.yameida.worktool.activity.GetScreenShotActivity
|
import org.yameida.worktool.activity.GetScreenShotActivity
|
||||||
import org.yameida.worktool.model.WeworkMessageBean
|
import org.yameida.worktool.model.WeworkMessageBean
|
||||||
@@ -38,7 +37,7 @@ object WeworkLoopImpl {
|
|||||||
while (mainLoopRunning) {
|
while (mainLoopRunning) {
|
||||||
// 如果正在等待回复,跳过新消息检测,避免多消息处理混乱
|
// 如果正在等待回复,跳过新消息检测,避免多消息处理混乱
|
||||||
if (WeworkController.waitingForReply) {
|
if (WeworkController.waitingForReply) {
|
||||||
LogUtils.d("等待回复中,暂停检测新消息...")
|
// LogUtils.d("等待回复中,暂停检测新消息...")
|
||||||
sleep(Constant.POP_WINDOW_INTERVAL)
|
sleep(Constant.POP_WINDOW_INTERVAL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -46,11 +45,10 @@ object WeworkLoopImpl {
|
|||||||
LogUtils.d("当前在房间: ")
|
LogUtils.d("当前在房间: ")
|
||||||
getChatMessageList()
|
getChatMessageList()
|
||||||
if (mainLoopRunning) {
|
if (mainLoopRunning) {
|
||||||
// 如果不是等待回复状态,则返回主页
|
|
||||||
if (!WeworkController.waitingForReply) {
|
if (!WeworkController.waitingForReply) {
|
||||||
goHome()
|
goHome()
|
||||||
} else {
|
} else {
|
||||||
LogUtils.d("等待回复中,保持当前页面...")
|
// LogUtils.d("等待回复中,保持当前页面...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -66,11 +64,13 @@ object WeworkLoopImpl {
|
|||||||
}
|
}
|
||||||
// 获取消息后,等待服务端回复指令,最多30秒
|
// 获取消息后,等待服务端回复指令,最多30秒
|
||||||
if (mainLoopRunning) {
|
if (mainLoopRunning) {
|
||||||
waitForServerReply()
|
// waitForServerReply()
|
||||||
}
|
}
|
||||||
// 等待完成后返回主页
|
// 等待完成后返回主页
|
||||||
if (mainLoopRunning) {
|
if (mainLoopRunning) {
|
||||||
goHome()
|
goHome()
|
||||||
|
// 返回主页后延迟一下,让界面稳定,避免快速重复检测
|
||||||
|
sleep(Constant.POP_WINDOW_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mainLoopRunning) break
|
if (!mainLoopRunning) break
|
||||||
@@ -143,7 +143,7 @@ object WeworkLoopImpl {
|
|||||||
val childCount = item.parent?.parent?.parent?.childCount
|
val childCount = item.parent?.parent?.parent?.childCount
|
||||||
if (childCount == 4 || childCount == 5) {
|
if (childCount == 4 || childCount == 5) {
|
||||||
if (item.parent != null && item.parent.childCount > 1) {
|
if (item.parent != null && item.parent.childCount > 1) {
|
||||||
LogUtils.d("通讯录有红点")
|
// LogUtils.d("通讯录有红点")
|
||||||
AccessibilityUtil.performClick(item)
|
AccessibilityUtil.performClick(item)
|
||||||
val hasRecommendFriend = AccessibilityUtil.findOneByText(getRoot(), "可能的同事", exact = true, timeout = Constant.POP_WINDOW_INTERVAL)
|
val hasRecommendFriend = AccessibilityUtil.findOneByText(getRoot(), "可能的同事", exact = true, timeout = Constant.POP_WINDOW_INTERVAL)
|
||||||
if (hasRecommendFriend != null) {
|
if (hasRecommendFriend != null) {
|
||||||
@@ -176,7 +176,6 @@ object WeworkLoopImpl {
|
|||||||
if (nameList.isEmpty())
|
if (nameList.isEmpty())
|
||||||
break
|
break
|
||||||
// todo 可自定义执行任务
|
// todo 可自定义执行任务
|
||||||
// Demo.test2(nameList[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -184,7 +183,7 @@ object WeworkLoopImpl {
|
|||||||
LogUtils.d("未发现待添加客户")
|
LogUtils.d("未发现待添加客户")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("通讯录无红点")
|
// LogUtils.v("通讯录无红点")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,7 +326,6 @@ object WeworkLoopImpl {
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
WeworkController.weworkService.webSocketManager.send(messageBean)
|
WeworkController.weworkService.webSocketManager.send(messageBean)
|
||||||
// 方案A:发送消息后立即返回主页,继续检测下一条消息,不等待
|
|
||||||
if (needInfer) {
|
if (needInfer) {
|
||||||
val lastMessage = messageList.lastOrNull()
|
val lastMessage = messageList.lastOrNull()
|
||||||
if (lastMessage != null && lastMessage.sender == 0) {
|
if (lastMessage != null && lastMessage.sender == 0) {
|
||||||
@@ -344,13 +342,10 @@ object WeworkLoopImpl {
|
|||||||
|| tempContent.isNotBlank()
|
|| tempContent.isNotBlank()
|
||||||
) {
|
) {
|
||||||
LogUtils.v("推测需要回复: $tempContent")
|
LogUtils.v("推测需要回复: $tempContent")
|
||||||
// 方案A:不清空 waitingForReply,直接返回主页
|
|
||||||
// 这样主循环会继续检测下一条消息
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
// 方案A:不清空 waitingForReply,直接返回主页
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else -> return true
|
else -> return true
|
||||||
@@ -458,11 +453,13 @@ object WeworkLoopImpl {
|
|||||||
if (textNode?.text?.toString() == "添加请求已过期,添加失败") {
|
if (textNode?.text?.toString() == "添加请求已过期,添加失败") {
|
||||||
LogUtils.d("添加好友失败")
|
LogUtils.d("添加好友失败")
|
||||||
} else {
|
} else {
|
||||||
// 获取完整的好友信息
|
val friend = WeworkMessageBean.Friend().apply {
|
||||||
val friendInfo = getFriendDetailInfo(friendName)
|
name = friendName
|
||||||
|
newFriend = true
|
||||||
|
}
|
||||||
val weworkMessageBean = WeworkMessageBean()
|
val weworkMessageBean = WeworkMessageBean()
|
||||||
weworkMessageBean.type = WeworkMessageBean.GET_FRIEND_INFO
|
weworkMessageBean.type = WeworkMessageBean.GET_FRIEND_INFO
|
||||||
weworkMessageBean.friend = friendInfo
|
weworkMessageBean.friend = friend
|
||||||
WeworkController.weworkService.webSocketManager.send(weworkMessageBean)
|
WeworkController.weworkService.webSocketManager.send(weworkMessageBean)
|
||||||
nameList.add(friendName)
|
nameList.add(friendName)
|
||||||
}
|
}
|
||||||
@@ -479,66 +476,6 @@ object WeworkLoopImpl {
|
|||||||
return nameList
|
return nameList
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取好友详细信息
|
|
||||||
* 通过好友后进入详情页抓取完整信息
|
|
||||||
*/
|
|
||||||
private fun getFriendDetailInfo(friendName: String): WeworkMessageBean.Friend {
|
|
||||||
val friend = WeworkMessageBean.Friend().apply {
|
|
||||||
name = friendName
|
|
||||||
newFriend = true
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 点击发消息进入聊天页面
|
|
||||||
val sendMsgNode = AccessibilityUtil.findOneByText(getRoot(), "发消息", exact = true)
|
|
||||||
if (sendMsgNode != null) {
|
|
||||||
AccessibilityUtil.performClick(sendMsgNode)
|
|
||||||
sleep(Constant.CHANGE_PAGE_INTERVAL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进入好友详情页
|
|
||||||
if (WeworkRoomUtil.intoFriendDetail()) {
|
|
||||||
sleep(Constant.CHANGE_PAGE_INTERVAL)
|
|
||||||
// 解析详情页信息
|
|
||||||
val listView = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
|
|
||||||
if (listView != null) {
|
|
||||||
// 遍历列表获取各字段信息
|
|
||||||
for (i in 0 until listView.childCount) {
|
|
||||||
val item = listView.getChild(i) ?: continue
|
|
||||||
val textViews = AccessibilityUtil.findAllOnceByClazz(item, Views.TextView)
|
|
||||||
if (textViews.size >= 2) {
|
|
||||||
val label = textViews[0].text?.toString() ?: ""
|
|
||||||
val value = textViews[1].text?.toString() ?: ""
|
|
||||||
when {
|
|
||||||
label.contains("微信号") || label.contains("微信ID") -> friend.wechatId = value
|
|
||||||
label.contains("手机") -> friend.phone = value
|
|
||||||
label.contains("企业") -> friend.corpName = value
|
|
||||||
label.contains("部门") -> friend.department = value
|
|
||||||
label.contains("职位") || label.contains("职务") -> friend.position = value
|
|
||||||
label.contains("邮箱") -> friend.email = value
|
|
||||||
label.contains("备注") -> friend.markName = value
|
|
||||||
}
|
|
||||||
LogUtils.d("好友详情 - $label: $value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 尝试获取头像(如果有)
|
|
||||||
val avatarView = AccessibilityUtil.findOneByClazz(getRoot(), Views.ImageView)
|
|
||||||
if (avatarView != null) {
|
|
||||||
// 头像通常没有直接URL,这里留空或可通过其他方式获取
|
|
||||||
// friend.avatarUrl = ""
|
|
||||||
}
|
|
||||||
// 返回聊天页面
|
|
||||||
backPress()
|
|
||||||
sleep(Constant.POP_WINDOW_INTERVAL)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtils.e("获取好友详情失败: ${e.message}")
|
|
||||||
}
|
|
||||||
LogUtils.d("好友信息: $friend")
|
|
||||||
return friend
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取单聊联系人详细信息
|
* 获取单聊联系人详细信息
|
||||||
*/
|
*/
|
||||||
@@ -588,7 +525,7 @@ object WeworkLoopImpl {
|
|||||||
if (!isAtHome()) { goHome() }
|
if (!isAtHome()) { goHome() }
|
||||||
|
|
||||||
if (logIndex++ % 30 == 0) {
|
if (logIndex++ % 30 == 0) {
|
||||||
LogUtils.d("读取首页聊天列表")
|
// LogUtils.d("读取首页聊天列表")
|
||||||
if (logIndex % 120 == 0) log("读取首页聊天列表")
|
if (logIndex % 120 == 0) log("读取首页聊天列表")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +567,7 @@ object WeworkLoopImpl {
|
|||||||
//消息不一致
|
//消息不一致
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("未发现新消息或无提示消息")
|
// LogUtils.v("未发现新消息或无提示消息")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -641,7 +578,7 @@ object WeworkLoopImpl {
|
|||||||
//让企微切换页面使APP保持活跃
|
//让企微切换页面使APP保持活跃
|
||||||
goHomeTab("通讯录")
|
goHomeTab("通讯录")
|
||||||
goHomeTab("消息")
|
goHomeTab("消息")
|
||||||
LogUtils.d("检查最近列表")
|
//LogUtils.d("检查最近列表")
|
||||||
log("检查最近列表")
|
log("检查最近列表")
|
||||||
if (hasNewMessage()) {
|
if (hasNewMessage()) {
|
||||||
return false
|
return false
|
||||||
@@ -699,7 +636,7 @@ object WeworkLoopImpl {
|
|||||||
* 检查首页-聊天列表是否有未读红点并点击进入
|
* 检查首页-聊天列表是否有未读红点并点击进入
|
||||||
*/
|
*/
|
||||||
private fun checkUnreadChatRoom(list: AccessibilityNodeInfo): Boolean {
|
private fun checkUnreadChatRoom(list: AccessibilityNodeInfo): Boolean {
|
||||||
val spotNodeList = arrayListOf<AccessibilityNodeInfo>()
|
val unreadRowNodeList = arrayListOf<AccessibilityNodeInfo>()
|
||||||
for (i in 0 until list.childCount) {
|
for (i in 0 until list.childCount) {
|
||||||
val item = list.getChild(i)
|
val item = list.getChild(i)
|
||||||
if (item != null && Views.RelativeLayout.equals(item.className)) {
|
if (item != null && Views.RelativeLayout.equals(item.className)) {
|
||||||
@@ -710,20 +647,22 @@ object WeworkLoopImpl {
|
|||||||
&& spotNode.text != null
|
&& spotNode.text != null
|
||||||
&& spotNode.text.toString().replace("+", "").isDigitsOnly()
|
&& spotNode.text.toString().replace("+", "").isDigitsOnly()
|
||||||
) {
|
) {
|
||||||
spotNodeList.add(spotNode)
|
unreadRowNodeList.add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spotNodeList.size > 0) {
|
if (unreadRowNodeList.isNotEmpty()) {
|
||||||
LogUtils.i("发现未读消息: " + spotNodeList.size + "条")
|
LogUtils.i("发现未读消息: " + unreadRowNodeList.size + "条")
|
||||||
log("发现未读消息: " + spotNodeList.size + "条")
|
log("发现未读消息: " + unreadRowNodeList.size + "条")
|
||||||
if (AccessibilityUtil.performClick(spotNodeList.firstOrNull())) {
|
val firstUnreadRow = unreadRowNodeList.firstOrNull()
|
||||||
//进入聊天页 下一步 getChatMessageList
|
val clicked = AccessibilityUtil.performClick(firstUnreadRow)
|
||||||
} else {
|
|| AccessibilityUtil.clickByNode(WeworkController.weworkService, firstUnreadRow)
|
||||||
AccessibilityUtil.clickByNode(WeworkController.weworkService, spotNodeList.firstOrNull()?.parent)
|
if (!clicked) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return true
|
// 避免“点击红点但未实际进会话”的假成功
|
||||||
|
return AccessibilityUtil.waitForPageMissing("WwMainActivity", "GlobalSearchActivity", timeout = 2000)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -747,7 +686,7 @@ object WeworkLoopImpl {
|
|||||||
if (tvList[2].contains("(退出了外部群)|(移出了群聊)|(邀请你加入了)|(修改群名为)|(此群为外部群)|(加入了外部群)".toRegex())) {
|
if (tvList[2].contains("(退出了外部群)|(移出了群聊)|(邀请你加入了)|(修改群名为)|(此群为外部群)|(加入了外部群)".toRegex())) {
|
||||||
val interval = System.currentTimeMillis() / 1000 - SPUtils.getInstance("noTipMessage").getLong(tvList[0], 0)
|
val interval = System.currentTimeMillis() / 1000 - SPUtils.getInstance("noTipMessage").getLong(tvList[0], 0)
|
||||||
if (interval > 3600) {
|
if (interval > 3600) {
|
||||||
LogUtils.i("发现无提示消息: $tvList")
|
// LogUtils.i("发现无提示消息: $tvList")
|
||||||
log("发现无提示消息: $tvList")
|
log("发现无提示消息: $tvList")
|
||||||
if (AccessibilityUtil.performClick(item)) {
|
if (AccessibilityUtil.performClick(item)) {
|
||||||
//进入聊天页 下一步 getChatMessageList
|
//进入聊天页 下一步 getChatMessageList
|
||||||
@@ -757,11 +696,11 @@ object WeworkLoopImpl {
|
|||||||
SPUtils.getInstance("noTipMessage").put(tvList[0], System.currentTimeMillis() / 1000)
|
SPUtils.getInstance("noTipMessage").put(tvList[0], System.currentTimeMillis() / 1000)
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("发现无提示消息: $tvList 消息在 $interval 秒前已被查看")
|
// LogUtils.v("发现无提示消息: $tvList 消息在 $interval 秒前已被查看")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("未发现无提示消息: ${tvList[1]}")
|
// LogUtils.v("未发现无提示消息: ${tvList[1]}")
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +734,7 @@ object WeworkLoopImpl {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (SPUtils.getInstance("noSyncMessage").getString(title) != lastSyncMessage) {
|
if (SPUtils.getInstance("noSyncMessage").getString(title) != lastSyncMessage) {
|
||||||
LogUtils.e("发现不一致消息: $tvList $lastSyncMessage")
|
// LogUtils.e("发现不一致消息: $tvList $lastSyncMessage")
|
||||||
error("发现不一致消息: $tvList $lastSyncMessage")
|
error("发现不一致消息: $tvList $lastSyncMessage")
|
||||||
SPUtils.getInstance("noSyncMessage").put(title, lastSyncMessage)
|
SPUtils.getInstance("noSyncMessage").put(title, lastSyncMessage)
|
||||||
if (AccessibilityUtil.performClick(item)) {
|
if (AccessibilityUtil.performClick(item)) {
|
||||||
@@ -805,10 +744,10 @@ object WeworkLoopImpl {
|
|||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("消息多次不一致: $tvList")
|
// LogUtils.v("消息多次不一致: $tvList")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LogUtils.v("未发现不一致消息: ${tvList[1]}")
|
// LogUtils.v("未发现不一致消息: ${tvList[1]}")
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,22 +45,6 @@ object WeworkOperationImpl {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证titleList是否在当前会话列表中存在
|
|
||||||
val currentRoomList = WeworkRoomUtil.getRoomTitle(print = false)
|
|
||||||
if (currentRoomList.isEmpty()) {
|
|
||||||
LogUtils.d("无法获取当前会话列表")
|
|
||||||
uploadCommandResult(message, ExecCallbackBean.ERROR_SEND_MESSAGE, "无法获取当前会话列表,请确保在微信首页", startTime, listOf(), titleList)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val notFoundList = titleList.filter { title ->
|
|
||||||
currentRoomList.none { it == title || it.replace(Constant.digitalRegex, "") == title || title.replace(Constant.digitalRegex, "") == it }
|
|
||||||
}
|
|
||||||
if (notFoundList.isNotEmpty()) {
|
|
||||||
LogUtils.d("以下会话不存在: ${notFoundList.joinToString()}")
|
|
||||||
uploadCommandResult(message, ExecCallbackBean.ERROR_SEND_MESSAGE, "会话不存在: ${notFoundList.joinToString()}", startTime, listOf(), titleList)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val successList = arrayListOf<String>()
|
val successList = arrayListOf<String>()
|
||||||
val failList = arrayListOf<String>()
|
val failList = arrayListOf<String>()
|
||||||
for (title in LinkedHashSet(titleList)) {
|
for (title in LinkedHashSet(titleList)) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.FileObserver
|
import android.os.FileObserver
|
||||||
|
import android.os.Message
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import com.blankj.utilcode.util.*
|
import com.blankj.utilcode.util.*
|
||||||
@@ -13,11 +14,10 @@ import okhttp3.Response
|
|||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import org.yameida.worktool.Constant
|
import org.yameida.worktool.Constant
|
||||||
import org.yameida.worktool.Demo
|
import org.yameida.worktool.model.WeworkMessageBean
|
||||||
import org.yameida.worktool.observer.MultiFileObserver
|
import org.yameida.worktool.observer.MultiFileObserver
|
||||||
import org.yameida.worktool.utils.*
|
import org.yameida.worktool.utils.*
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业微信辅助服务
|
* 企业微信辅助服务
|
||||||
@@ -39,24 +39,23 @@ class WeworkService : AccessibilityService() {
|
|||||||
LogUtils.i("初始化成功")
|
LogUtils.i("初始化成功")
|
||||||
//隐藏软键盘模式
|
//隐藏软键盘模式
|
||||||
softKeyboardController.showMode = SHOW_MODE_HIDDEN
|
softKeyboardController.showMode = SHOW_MODE_HIDDEN
|
||||||
|
// 服务重启后恢复主功能默认运行态,避免被上一次暂停状态卡住
|
||||||
|
FloatWindowHelper.isPause = false
|
||||||
WeworkController.weworkService = this
|
WeworkController.weworkService = this
|
||||||
WeworkController.enableLoopRunning = true
|
WeworkController.enableLoopRunning = true
|
||||||
|
notifyAccessibilityState(true)
|
||||||
//初始化长连接
|
//初始化长连接
|
||||||
initWebSocket()
|
initWebSocket()
|
||||||
//初始化消息处理器
|
//初始化消息处理器
|
||||||
MyLooper.init()
|
MyLooper.init()
|
||||||
//初始化图片接收
|
//初始化图片接收
|
||||||
initObserver()
|
initObserver()
|
||||||
//开发者可以在这里添加测试代码 启动时调用一次
|
|
||||||
thread { Demo.test(AppUtils.isAppDebug()) }
|
|
||||||
|
|
||||||
//监听是否修改链接号并重新长连接
|
//监听是否修改链接号并重新长连接
|
||||||
registerReceiver(object : BroadcastReceiver() {
|
registerReceiver(object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.getStringExtra("type") == "modify_channel") {
|
if (intent.getStringExtra("type") == "modify_channel") {
|
||||||
LogUtils.e("更新channel")
|
reconnectWebSocket("modify_channel")
|
||||||
webSocketManager.close(1000, "modify_channel")
|
|
||||||
initWebSocket()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, IntentFilter(Constant.WEWORK_NOTIFY))
|
}, IntentFilter(Constant.WEWORK_NOTIFY))
|
||||||
@@ -69,6 +68,14 @@ class WeworkService : AccessibilityService() {
|
|||||||
webSocketManager = WebSocketManager(url, listener)
|
webSocketManager = WebSocketManager(url, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reconnectWebSocket(reason: String) {
|
||||||
|
LogUtils.i("reconnectWebSocket: $reason")
|
||||||
|
if (::webSocketManager.isInitialized) {
|
||||||
|
webSocketManager.close(1000, reason)
|
||||||
|
}
|
||||||
|
initWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initObserver() {
|
private fun initObserver() {
|
||||||
if (!Constant.pushImage) return
|
if (!Constant.pushImage) return
|
||||||
try {
|
try {
|
||||||
@@ -113,16 +120,24 @@ class WeworkService : AccessibilityService() {
|
|||||||
LogUtils.i("onDestroy")
|
LogUtils.i("onDestroy")
|
||||||
//关闭自动回复
|
//关闭自动回复
|
||||||
WeworkController.enableLoopRunning = false
|
WeworkController.enableLoopRunning = false
|
||||||
|
// 关闭主功能时清理暂停态,确保下次开启可立即进入检测
|
||||||
|
FloatWindowHelper.isPause = false
|
||||||
|
notifyAccessibilityState(false)
|
||||||
//隐藏软键盘模式
|
//隐藏软键盘模式
|
||||||
softKeyboardController.showMode = SHOW_MODE_AUTO
|
softKeyboardController.showMode = SHOW_MODE_AUTO
|
||||||
webSocketManager.close(1000, "service Destroy")
|
webSocketManager.close(1000, "service Destroy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyAccessibilityState(enabled: Boolean) {
|
||||||
|
sendBroadcast(Intent(Constant.WEWORK_NOTIFY).apply {
|
||||||
|
putExtra("type", "accessibility_state")
|
||||||
|
putExtra("enabled", enabled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
inner class EchoWebSocketListener : WebSocketListener() {
|
inner class EchoWebSocketListener : WebSocketListener() {
|
||||||
private val TAG = "WeworkService.EchoWebSocketListener"
|
private val TAG = "WeworkService.EchoWebSocketListener"
|
||||||
private lateinit var socket: WebSocket
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
socket = webSocket
|
|
||||||
Log.e(TAG, "连接建立")
|
Log.e(TAG, "连接建立")
|
||||||
val robotId = Constant.robotId
|
val robotId = Constant.robotId
|
||||||
val appVersion = SPUtils.getInstance().getString("appVersion", "")
|
val appVersion = SPUtils.getInstance().getString("appVersion", "")
|
||||||
@@ -131,6 +146,13 @@ class WeworkService : AccessibilityService() {
|
|||||||
val hook = SPUtils.getInstance().getBoolean("hook", false)
|
val hook = SPUtils.getInstance().getBoolean("hook", false)
|
||||||
LogUtils.i("连接建立: $robotId appVersion: $appVersion workVersion: $workVersion deviceRooted: $deviceRooted hook: $hook")
|
LogUtils.i("连接建立: $robotId appVersion: $appVersion workVersion: $workVersion deviceRooted: $deviceRooted hook: $hook")
|
||||||
log("连接建立: $robotId appVersion: $appVersion workVersion: $workVersion deviceRooted: $deviceRooted hook: $hook")
|
log("连接建立: $robotId appVersion: $appVersion workVersion: $workVersion deviceRooted: $deviceRooted hook: $hook")
|
||||||
|
// 断线后会被置为false;重连成功时主动恢复主循环,避免出现“提示运行中但未检测消息”
|
||||||
|
WeworkController.enableLoopRunning = true
|
||||||
|
MyLooper.getInstance().removeMessages(WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE)
|
||||||
|
MyLooper.getInstance().sendMessage(Message.obtain().apply {
|
||||||
|
what = WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE
|
||||||
|
obj = WeworkMessageBean().apply { type = WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE }
|
||||||
|
})
|
||||||
LogUtils.i("设置自动跳转企业微信")
|
LogUtils.i("设置自动跳转企业微信")
|
||||||
sendBroadcast(true)
|
sendBroadcast(true)
|
||||||
}
|
}
|
||||||
@@ -154,7 +176,7 @@ class WeworkService : AccessibilityService() {
|
|||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
super.onClosing(webSocket, code, reason)
|
super.onClosing(webSocket, code, reason)
|
||||||
socket.close(code, reason)
|
webSocket.close(code, reason)
|
||||||
Log.e(TAG, "服务端关闭连接 $code: $reason")
|
Log.e(TAG, "服务端关闭连接 $code: $reason")
|
||||||
sendBroadcast(false)
|
sendBroadcast(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import kotlin.concurrent.thread
|
|||||||
object FloatWindowHelper {
|
object FloatWindowHelper {
|
||||||
|
|
||||||
var isPause = false
|
var isPause = false
|
||||||
|
private var bound = false
|
||||||
|
|
||||||
fun showWindow() {
|
fun showWindow() {
|
||||||
LogUtils.d("FloatWindowHelper.showWindow()")
|
LogUtils.d("FloatWindowHelper.showWindow()")
|
||||||
@@ -39,7 +40,23 @@ object FloatWindowHelper {
|
|||||||
|
|
||||||
val app = Utils.getApp()
|
val app = Utils.getApp()
|
||||||
val intent = Intent(app, DefaultFloatService::class.java)
|
val intent = Intent(app, DefaultFloatService::class.java)
|
||||||
app.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
if (!bound) {
|
||||||
|
bound = app.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideWindow() {
|
||||||
|
LogUtils.d("FloatWindowHelper.hideWindow()")
|
||||||
|
val app = Utils.getApp()
|
||||||
|
FloatWindowManager.hide(DefaultFloatService::class.java)
|
||||||
|
if (bound) {
|
||||||
|
try {
|
||||||
|
app.unbindService(serviceConnection)
|
||||||
|
} catch (ignore: Exception) {
|
||||||
|
}
|
||||||
|
bound = false
|
||||||
|
}
|
||||||
|
app.stopService(Intent(app, DefaultFloatService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,35 +102,6 @@ object FloatWindowHelper {
|
|||||||
service.onClickListener = object : OnClickListener {
|
service.onClickListener = object : OnClickListener {
|
||||||
override fun onClick(v: View, event: Int) {
|
override fun onClick(v: View, event: Int) {
|
||||||
when (event) {
|
when (event) {
|
||||||
1 -> {
|
|
||||||
if (PermissionHelper.isAccessibilitySettingOn()) {
|
|
||||||
if (!isPause) {
|
|
||||||
ToastUtils.showShort("请先暂停Awin WorkTool主功能~")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
thread {
|
|
||||||
val printNodeClazzTree =
|
|
||||||
AccessibilityUtil.printNodeClazzTree(getRoot(true))
|
|
||||||
val df = SimpleDateFormat("MMdd_HHmmss")
|
|
||||||
val filePath = "${
|
|
||||||
Utils.getApp().getExternalFilesDir("share")
|
|
||||||
}/${df.format(Date())}/${df.format(Date())}_printNode.txt"
|
|
||||||
val newFile = File(filePath)
|
|
||||||
val create = FileUtils.createFileByDeleteOldFile(newFile)
|
|
||||||
if (create && newFile.canWrite()) {
|
|
||||||
printNodeClazzTree.append("\n")
|
|
||||||
.append(WeworkController.weworkService.currentPackage)
|
|
||||||
.append("\n")
|
|
||||||
.append(WeworkController.weworkService.currentClass)
|
|
||||||
newFile.writeBytes(printNodeClazzTree.toString().toByteArray())
|
|
||||||
LogUtils.i("打印节点文件存储本地成功 $filePath", "当前页面: ${WeworkController.weworkService.currentClass}")
|
|
||||||
}
|
|
||||||
ShareUtil.share("*/*", newFile)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ToastUtils.showShort("请先打开Awin WorkTool主功能~")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2 -> {
|
2 -> {
|
||||||
if (PermissionHelper.isAccessibilitySettingOn()) {
|
if (PermissionHelper.isAccessibilitySettingOn()) {
|
||||||
if (isPause) {
|
if (isPause) {
|
||||||
@@ -127,6 +115,28 @@ object FloatWindowHelper {
|
|||||||
ToastUtils.showShort("请先打开Awin WorkTool主功能~")
|
ToastUtils.showShort("请先打开Awin WorkTool主功能~")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5 -> {
|
||||||
|
if (PermissionHelper.isAccessibilitySettingOn()) {
|
||||||
|
if (isPause) {
|
||||||
|
Glide.with(Utils.getApp()).load(R.drawable.float_icon_pause).into(v as ImageView)
|
||||||
|
accessibilityServiceResume()
|
||||||
|
} else {
|
||||||
|
Glide.with(Utils.getApp()).load(R.drawable.float_icon_play).into(v as ImageView)
|
||||||
|
accessibilityServicePause()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastUtils.showShort("请先打开Awin WorkTool主功能~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6 -> {
|
||||||
|
if (PermissionHelper.isAccessibilitySettingOn()) {
|
||||||
|
// 停止主功能时复位暂停状态,避免下次开启后主循环被卡住
|
||||||
|
isPause = false
|
||||||
|
WeworkController.weworkService.disableSelf()
|
||||||
|
} else {
|
||||||
|
ToastUtils.showShort("主功能已关闭~")
|
||||||
|
}
|
||||||
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
Utils.getApp().packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
|
Utils.getApp().packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
|
||||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
@@ -143,6 +153,7 @@ object FloatWindowHelper {
|
|||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
LogUtils.i("DefaultFloatService 服务断开")
|
LogUtils.i("DefaultFloatService 服务断开")
|
||||||
|
bound = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ object HttpUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(response: Response<String>) {
|
override fun onError(response: Response<String>) {
|
||||||
ToastUtils.showLong("获取配置失败 请检查机器人ID")
|
if (toast) {
|
||||||
LogUtils.e("获取配置失败 请检查机器人ID")
|
ToastUtils.showLong("获取配置失败,请检查网络/Host/机器人ID")
|
||||||
|
}
|
||||||
|
LogUtils.e("获取配置失败: ${response.code()} ${response.exception?.message}")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,32 +22,37 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
import okhttp3.WebSocket;
|
import okhttp3.WebSocket;
|
||||||
import okhttp3.WebSocketListener;
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
public class WebSocketManager {
|
public class WebSocketManager {
|
||||||
|
|
||||||
public static final String HEARTBEAT = "{\"type\":" + WeworkMessageBean.HEART_BEAT + "}";
|
public static final String HEARTBEAT = "{\"type\":" + WeworkMessageBean.HEART_BEAT + "}";
|
||||||
private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
private static final OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.pingInterval(25, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build();
|
||||||
public static Map<String, WebSocketManager> webSocketManager = new ConcurrentHashMap<>();
|
public static Map<String, WebSocketManager> webSocketManager = new ConcurrentHashMap<>();
|
||||||
private static final int reconnectInt = 5000; //毫秒
|
|
||||||
private static final long heartBeatRate = 5; //秒
|
private static final long heartBeatRate = 5; //秒
|
||||||
private Map<String, Long> messageIdMap = new ConcurrentHashMap<>();
|
private Map<String, Long> messageIdMap = new ConcurrentHashMap<>();
|
||||||
private ScheduledFuture task;
|
private ScheduledFuture task;
|
||||||
private WebSocket socket;
|
private WebSocket socket;
|
||||||
private String url;
|
private String url;
|
||||||
private WebSocketListener listener;
|
private WebSocketListener listener;
|
||||||
private boolean connecting = false;
|
private volatile boolean connecting = false;
|
||||||
|
private volatile boolean manuallyClosed = false;
|
||||||
|
private volatile boolean opened = false;
|
||||||
private long lastConnectedTime = 0L;
|
private long lastConnectedTime = 0L;
|
||||||
|
|
||||||
public WebSocketManager(String url, WebSocketListener listener) {
|
public WebSocketManager(String url, WebSocketListener listener) {
|
||||||
Log.e(url, "新建链接");
|
Log.e(url, "新建链接");
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
OkHttpClient client = new OkHttpClient();
|
this.socket = client.newWebSocket(new Request.Builder().url(url).build(), innerListener);
|
||||||
Request request = new Request.Builder().url(url).build();
|
|
||||||
this.socket = client.newWebSocket(request, listener);
|
|
||||||
socket.send("{\"td\":" + System.currentTimeMillis() + "}");
|
|
||||||
webSocketManager.put(url, this);
|
webSocketManager.put(url, this);
|
||||||
task = heartCheckStart();
|
task = heartCheckStart();
|
||||||
}
|
}
|
||||||
@@ -83,7 +88,7 @@ public class WebSocketManager {
|
|||||||
|
|
||||||
public void send(WeworkMessageListBean msg, boolean log) {
|
public void send(WeworkMessageListBean msg, boolean log) {
|
||||||
String json = GsonUtils.toJson(msg);
|
String json = GsonUtils.toJson(msg);
|
||||||
boolean success = socket.send(json);
|
boolean success = socket != null && socket.send(json);
|
||||||
if (log && success)
|
if (log && success)
|
||||||
LogUtils.d(url, json, "通讯消息发送成功!");
|
LogUtils.d(url, json, "通讯消息发送成功!");
|
||||||
if (!success)
|
if (!success)
|
||||||
@@ -91,14 +96,19 @@ public class WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void send(String msg) {
|
public void send(String msg) {
|
||||||
boolean success = socket.send(msg);
|
boolean success = socket != null && socket.send(msg);
|
||||||
LogUtils.e(url, msg, (success ? "通讯消息发送成功!" : "通讯消息发送失败!"));
|
LogUtils.e(url, msg, (success ? "通讯消息发送成功!" : "通讯消息发送失败!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close(int code, String reason) {
|
public void close(int code, String reason) {
|
||||||
|
manuallyClosed = true;
|
||||||
|
if (task != null) {
|
||||||
task.cancel(true);
|
task.cancel(true);
|
||||||
|
}
|
||||||
Log.e("url", "task 取消");
|
Log.e("url", "task 取消");
|
||||||
|
if (this.socket != null) {
|
||||||
this.socket.close(code, reason);
|
this.socket.close(code, reason);
|
||||||
|
}
|
||||||
Log.e(url, "链接关闭");
|
Log.e(url, "链接关闭");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,44 +122,25 @@ public class WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void reConnect() {
|
public void reConnect() {
|
||||||
|
if (manuallyClosed || connecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
connecting = true;
|
connecting = true;
|
||||||
|
opened = false;
|
||||||
Log.e(url, "重连");
|
Log.e(url, "重连");
|
||||||
boolean isConnect = false;
|
|
||||||
int interval = reconnectInt;
|
|
||||||
while (true) {
|
|
||||||
try {
|
try {
|
||||||
isConnect = connect();
|
if (socket != null) {
|
||||||
if (isConnect) {
|
socket.cancel();
|
||||||
connecting = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception ignore) {
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
try {
|
socket = client.newWebSocket(new Request.Builder().url(url).build(), innerListener);
|
||||||
Thread.sleep(interval);
|
|
||||||
if (interval < 600000) {
|
|
||||||
interval *= 2;
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean connect() {
|
|
||||||
WebSocket s = new OkHttpClient().newWebSocket(new Request.Builder().url(url).build(), listener);
|
|
||||||
if (s.send(WebSocketManager.HEARTBEAT)) {
|
|
||||||
this.socket = s;
|
|
||||||
s.send("{\"td\":" + System.currentTimeMillis() + "}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledFuture heartCheckStart() {
|
private ScheduledFuture heartCheckStart() {
|
||||||
lastConnectedTime = System.currentTimeMillis();
|
lastConnectedTime = System.currentTimeMillis();
|
||||||
Runnable r = () -> {
|
Runnable r = () -> {
|
||||||
|
if (manuallyClosed) return;
|
||||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
|
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
|
||||||
Log.d(url, "心跳检测" + df.format(new Date()));// new Date()为获取当前系统时间
|
Log.d(url, "心跳检测" + df.format(new Date()));// new Date()为获取当前系统时间
|
||||||
if (!connecting && (socket == null || !socket.send(HEARTBEAT))) {
|
if (!connecting && (socket == null || !socket.send(HEARTBEAT))) {
|
||||||
@@ -159,7 +150,7 @@ public class WebSocketManager {
|
|||||||
reConnect();
|
reConnect();
|
||||||
//重连后刷新连接时间
|
//重连后刷新连接时间
|
||||||
lastConnectedTime = System.currentTimeMillis();
|
lastConnectedTime = System.currentTimeMillis();
|
||||||
} else if (System.currentTimeMillis() % 1000 == 0) {
|
} else if (opened && System.currentTimeMillis() % 1000 == 0) {
|
||||||
socket.send("{\"td\":" + System.currentTimeMillis() + "}");
|
socket.send("{\"td\":" + System.currentTimeMillis() + "}");
|
||||||
}
|
}
|
||||||
if (!Constant.INSTANCE.getEnableMediaProject()) {
|
if (!Constant.INSTANCE.getEnableMediaProject()) {
|
||||||
@@ -176,4 +167,46 @@ public class WebSocketManager {
|
|||||||
public static WebSocketManager getWebSocketManager(String id) {
|
public static WebSocketManager getWebSocketManager(String id) {
|
||||||
return webSocketManager.get(id);
|
return webSocketManager.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final WebSocketListener innerListener = new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket, Response response) {
|
||||||
|
opened = true;
|
||||||
|
connecting = false;
|
||||||
|
lastConnectedTime = System.currentTimeMillis();
|
||||||
|
listener.onOpen(webSocket, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
|
listener.onMessage(webSocket, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||||
|
listener.onMessage(webSocket, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosing(WebSocket webSocket, int code, String reason) {
|
||||||
|
opened = false;
|
||||||
|
connecting = false;
|
||||||
|
listener.onClosing(webSocket, code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||||
|
opened = false;
|
||||||
|
connecting = false;
|
||||||
|
listener.onClosed(webSocket, code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||||
|
opened = false;
|
||||||
|
connecting = false;
|
||||||
|
WeworkController.INSTANCE.setEnableLoopRunning(false);
|
||||||
|
listener.onFailure(webSocket, t, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,7 +329,12 @@ object WeworkTextUtil {
|
|||||||
* 是否为文件上方时间
|
* 是否为文件上方时间
|
||||||
*/
|
*/
|
||||||
fun isFileSize(size: String?): Boolean {
|
fun isFileSize(size: String?): Boolean {
|
||||||
return size?.matches("[0-9\\.]+[BKMG]".toRegex()) ?: false
|
if (size.isNullOrBlank()) return false
|
||||||
|
// 兼容常见文件大小格式: 12M / 12MB / 12.5 MB / 12 kb
|
||||||
|
val normalized = size.trim()
|
||||||
|
return normalized.matches(
|
||||||
|
"^[0-9]+(?:\\.[0-9]+)?[\\s\\u00A0]?(?:B|KB|MB|GB|TB|K|M|G)$".toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -326,11 +326,14 @@
|
|||||||
android:textColor="@color/color_333333"
|
android:textColor="@color/color_333333"
|
||||||
android:textSize="@dimen/setting_start_font_size" />
|
android:textSize="@dimen/setting_start_font_size" />
|
||||||
|
|
||||||
<EditText
|
<TextView
|
||||||
android:id="@+id/et_channel"
|
android:id="@+id/tv_channel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="请输入申请的机器人ID"
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:text="未生成"
|
||||||
|
android:textIsSelectable="true"
|
||||||
android:textColor="@color/color_999999"
|
android:textColor="@color/color_999999"
|
||||||
android:textSize="@dimen/setting_end_font_size" />
|
android:textSize="@dimen/setting_end_font_size" />
|
||||||
|
|
||||||
@@ -344,17 +347,17 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/bt_save"
|
android:id="@+id/bt_reset_channel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:background="@drawable/comment_red_btn"
|
android:background="@drawable/comment_red_btn"
|
||||||
android:paddingStart="50dp"
|
android:paddingStart="42dp"
|
||||||
android:paddingEnd="50dp"
|
android:paddingEnd="42dp"
|
||||||
android:textSize="18sp"
|
android:text="重置"
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:text="保存" />
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -473,172 +473,6 @@
|
|||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/rl_update"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/setting_start_padding"
|
|
||||||
android:paddingTop="@dimen/setting_vertical_padding"
|
|
||||||
android:paddingEnd="@dimen/setting_end_padding"
|
|
||||||
android:paddingBottom="@dimen/setting_vertical_padding">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_rec_update_"
|
|
||||||
android:layout_width="@dimen/setting_start_image_width"
|
|
||||||
android:layout_height="@dimen/setting_start_image_width"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:scaleX="1.1"
|
|
||||||
android:scaleY="1.1"
|
|
||||||
android:src="@drawable/settings_directory" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_select_update"
|
|
||||||
android:layout_width="@dimen/setting_end_font_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_end_start_padding"
|
|
||||||
android:textColor="@color/float_time_color"
|
|
||||||
android:textSize="@dimen/setting_end_font_size"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_start_padding"
|
|
||||||
android:layout_toStartOf="@id/tv_select_update"
|
|
||||||
android:layout_toEndOf="@id/iv_rec_update_"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="检查新版本"
|
|
||||||
android:textColor="@color/color_333333"
|
|
||||||
android:textSize="@dimen/setting_start_font_size" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="版本更新检查"
|
|
||||||
android:textColor="@color/color_999999"
|
|
||||||
android:textSize="@dimen/setting_end_font_size" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/rl_donate"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/setting_start_padding"
|
|
||||||
android:paddingTop="@dimen/setting_vertical_padding"
|
|
||||||
android:paddingEnd="@dimen/setting_end_padding"
|
|
||||||
android:paddingBottom="@dimen/setting_vertical_padding">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_rec_donate_"
|
|
||||||
android:layout_width="@dimen/setting_start_image_width"
|
|
||||||
android:layout_height="@dimen/setting_start_image_width"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:src="@drawable/settings_rate_us" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_select_donate"
|
|
||||||
android:layout_width="@dimen/setting_end_font_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_end_start_padding"
|
|
||||||
android:textColor="@color/float_time_color"
|
|
||||||
android:textSize="@dimen/setting_end_font_size"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_start_padding"
|
|
||||||
android:layout_toStartOf="@id/tv_select_donate"
|
|
||||||
android:layout_toEndOf="@id/iv_rec_donate_"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="赞助我们"
|
|
||||||
android:textColor="@color/color_333333"
|
|
||||||
android:textSize="@dimen/setting_start_font_size" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="还有机会成为我们的内测用户体验新功能"
|
|
||||||
android:textColor="@color/color_999999"
|
|
||||||
android:textSize="@dimen/setting_end_font_size" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/rl_share"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/setting_start_padding"
|
|
||||||
android:paddingTop="@dimen/setting_vertical_padding"
|
|
||||||
android:paddingEnd="@dimen/setting_end_padding"
|
|
||||||
android:paddingBottom="@dimen/setting_vertical_padding">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_rec_share_"
|
|
||||||
android:layout_width="@dimen/setting_start_image_width"
|
|
||||||
android:layout_height="@dimen/setting_start_image_width"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:scaleX="1.1"
|
|
||||||
android:scaleY="1.1"
|
|
||||||
android:src="@drawable/settings_share" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_select_share"
|
|
||||||
android:layout_width="@dimen/setting_end_font_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_end_start_padding"
|
|
||||||
android:textColor="@color/float_time_color"
|
|
||||||
android:textSize="@dimen/setting_end_font_size"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/setting_start_padding"
|
|
||||||
android:layout_toStartOf="@id/tv_select_share"
|
|
||||||
android:layout_toEndOf="@id/iv_rec_share_"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="分享应用"
|
|
||||||
android:textColor="@color/color_333333"
|
|
||||||
android:textSize="@dimen/setting_start_font_size" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="把本应用分享给其他人"
|
|
||||||
android:textColor="@color/color_999999"
|
|
||||||
android:textSize="@dimen/setting_end_font_size" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|||||||
@@ -317,6 +317,53 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/rl_host"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/setting_start_padding"
|
||||||
|
android:paddingTop="@dimen/setting_vertical_padding"
|
||||||
|
android:paddingEnd="@dimen/setting_end_padding"
|
||||||
|
android:paddingBottom="@dimen/setting_vertical_padding">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_select_host"
|
||||||
|
android:layout_width="@dimen/setting_end_font_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="@dimen/setting_end_start_padding"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/float_time_color"
|
||||||
|
android:textSize="@dimen/setting_end_font_size"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="@dimen/setting_start_padding"
|
||||||
|
android:layout_toStartOf="@id/tv_select_host"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="服务器地址"
|
||||||
|
android:textColor="@color/color_333333"
|
||||||
|
android:textSize="@dimen/setting_start_font_size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="WebSocket服务器地址(ws://或wss://开头)"
|
||||||
|
android:textColor="@color/color_999999"
|
||||||
|
android:textSize="@dimen/setting_end_font_size" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/rl_rec_orientation"
|
android:id="@+id/rl_rec_orientation"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -56,7 +56,14 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
|
|||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
LogUtils.d(TAG, "onStartCommand: ${intent?.data}")
|
LogUtils.d(TAG, "onStartCommand: ${intent?.data}")
|
||||||
show()
|
when (intent?.getStringExtra(FloatWindowManager.EXTRA_ACTION)) {
|
||||||
|
FloatWindowManager.ACTION_HIDE -> {
|
||||||
|
hide()
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
else -> show()
|
||||||
|
}
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +88,6 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
|
|||||||
if (v == leftView.iv_logo_left || v == rightView.iv_logo_right || v == leftView.iv_logo_left2 || v == rightView.iv_logo_right2) {
|
if (v == leftView.iv_logo_left || v == rightView.iv_logo_right || v == leftView.iv_logo_left2 || v == rightView.iv_logo_right2) {
|
||||||
onClickListener?.onClick(v, 0)
|
onClickListener?.onClick(v, 0)
|
||||||
}
|
}
|
||||||
if (v == leftView.iv_start_left || v == rightView.iv_start_right) {
|
|
||||||
onClickListener?.onClick(v,1)
|
|
||||||
}
|
|
||||||
if (v == leftView.iv_shot_left || v == rightView.iv_shot_right) {
|
if (v == leftView.iv_shot_left || v == rightView.iv_shot_right) {
|
||||||
onClickListener?.onClick(v,2)
|
onClickListener?.onClick(v,2)
|
||||||
}
|
}
|
||||||
@@ -105,7 +109,6 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
|
|||||||
leftView = inflater.inflate(R.layout.layout_menu_left, null)
|
leftView = inflater.inflate(R.layout.layout_menu_left, null)
|
||||||
leftView.iv_logo_left.setOnClickListener(this)
|
leftView.iv_logo_left.setOnClickListener(this)
|
||||||
leftView.iv_logo_left2.setOnClickListener(this)
|
leftView.iv_logo_left2.setOnClickListener(this)
|
||||||
leftView.iv_start_left.setOnClickListener(this)
|
|
||||||
leftView.iv_shot_left.setOnClickListener(this)
|
leftView.iv_shot_left.setOnClickListener(this)
|
||||||
leftView.iv_back_left.setOnClickListener(this)
|
leftView.iv_back_left.setOnClickListener(this)
|
||||||
leftView.iv_resume_pause_left.setOnClickListener(this)
|
leftView.iv_resume_pause_left.setOnClickListener(this)
|
||||||
@@ -119,7 +122,6 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
|
|||||||
rightView = inflater.inflate(R.layout.layout_menu_right, null)
|
rightView = inflater.inflate(R.layout.layout_menu_right, null)
|
||||||
rightView.iv_logo_right.setOnClickListener(this)
|
rightView.iv_logo_right.setOnClickListener(this)
|
||||||
rightView.iv_logo_right2.setOnClickListener(this)
|
rightView.iv_logo_right2.setOnClickListener(this)
|
||||||
rightView.iv_start_right.setOnClickListener(this)
|
|
||||||
rightView.iv_shot_right.setOnClickListener(this)
|
rightView.iv_shot_right.setOnClickListener(this)
|
||||||
rightView.iv_back_right.setOnClickListener(this)
|
rightView.iv_back_right.setOnClickListener(this)
|
||||||
rightView.iv_resume_pause_right.setOnClickListener(this)
|
rightView.iv_resume_pause_right.setOnClickListener(this)
|
||||||
@@ -197,4 +199,10 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyed() {}
|
override fun onDestroyed() {}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
hide()
|
||||||
|
stopForeground(true)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ object FloatWindowManager {
|
|||||||
private val TAG = FloatWindowManager::class.java.simpleName
|
private val TAG = FloatWindowManager::class.java.simpleName
|
||||||
|
|
||||||
private var context = Utils.getApp()
|
private var context = Utils.getApp()
|
||||||
|
const val EXTRA_ACTION = "float_action"
|
||||||
|
const val ACTION_SHOW = "show"
|
||||||
|
const val ACTION_HIDE = "hide"
|
||||||
|
|
||||||
fun show(service: Class<out BaseFloatWindow>, intent: Intent? = null) {
|
fun show(service: Class<out BaseFloatWindow>, intent: Intent? = null) {
|
||||||
startServiceSafe(Intent(context, service).apply {
|
startServiceSafe(Intent(context, service).apply {
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_SHOW)
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
this.putExtras(intent)
|
this.putExtras(intent)
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,7 @@ object FloatWindowManager {
|
|||||||
|
|
||||||
fun hide(service: Class<out BaseFloatWindow>, intent: Intent? = null) {
|
fun hide(service: Class<out BaseFloatWindow>, intent: Intent? = null) {
|
||||||
startServiceSafe(Intent(context, service).apply {
|
startServiceSafe(Intent(context, service).apply {
|
||||||
|
putExtra(EXTRA_ACTION, ACTION_HIDE)
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
this.putExtras(intent)
|
this.putExtras(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,12 @@
|
|||||||
android:visibility="visible">
|
android:visibility="visible">
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_start_left"
|
|
||||||
android:layout_width="@dimen/float_size"
|
|
||||||
android:layout_height="@dimen/float_size"
|
|
||||||
android:layout_marginEnd="@dimen/float_margin_start"
|
|
||||||
android:src="@drawable/float_icon_record" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_shot_left"
|
android:id="@+id/iv_shot_left"
|
||||||
android:layout_width="@dimen/float_size"
|
android:layout_width="@dimen/float_size"
|
||||||
android:layout_height="@dimen/float_size"
|
android:layout_height="@dimen/float_size"
|
||||||
android:layout_marginStart="@dimen/float_margin_start"
|
android:layout_marginStart="@dimen/float_margin_start"
|
||||||
android:layout_marginTop="29dp"
|
android:layout_marginTop="0dp"
|
||||||
android:src="@drawable/float_icon_pause" />
|
android:src="@drawable/float_icon_pause" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -44,7 +37,7 @@
|
|||||||
android:id="@+id/iv_logo_left"
|
android:id="@+id/iv_logo_left"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_below="@id/iv_start_left"
|
android:layout_below="@id/iv_shot_left"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginEnd="55dp"
|
android:layout_marginEnd="55dp"
|
||||||
|
|||||||
@@ -15,18 +15,11 @@
|
|||||||
android:layout_height="190dp"
|
android:layout_height="190dp"
|
||||||
android:visibility="visible">
|
android:visibility="visible">
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_start_right"
|
|
||||||
android:layout_width="@dimen/float_size"
|
|
||||||
android:layout_height="@dimen/float_size"
|
|
||||||
android:layout_marginStart="@dimen/float_margin_start"
|
|
||||||
android:src="@drawable/float_icon_record" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_shot_right"
|
android:id="@+id/iv_shot_right"
|
||||||
android:layout_width="@dimen/float_size"
|
android:layout_width="@dimen/float_size"
|
||||||
android:layout_height="@dimen/float_size"
|
android:layout_height="@dimen/float_size"
|
||||||
android:layout_marginTop="29dp"
|
android:layout_marginTop="0dp"
|
||||||
android:src="@drawable/float_icon_pause" />
|
android:src="@drawable/float_icon_pause" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -41,7 +34,7 @@
|
|||||||
android:id="@+id/iv_logo_right"
|
android:id="@+id/iv_logo_right"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_below="@id/iv_start_right"
|
android:layout_below="@id/iv_shot_right"
|
||||||
android:layout_marginStart="55dp"
|
android:layout_marginStart="55dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:src="@mipmap/ic_launcher_round" />
|
android:src="@mipmap/ic_launcher_round" />
|
||||||
|
|||||||
421
交接文档.md
Normal file
421
交接文档.md
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
# WorkTool 项目交接文档
|
||||||
|
|
||||||
|
本文档面向后续接手的开发人员,帮助快速理解 WorkTool 的整体架构、运行机制以及关键代码位置,便于后续维护与二次开发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
- 项目名称:WorkTool
|
||||||
|
- 主要功能:依附于企业微信 / 微信运行的无人值守群管理机器人,通过无障碍服务和企业微信官方 SDK 实现自动收发消息、建群、拉人、踢人等能力。
|
||||||
|
- 客户端形态:Android 原生 APP(需运行在一台专用手机上),通过 WebSocket 与后端任务调度平台长连接通信。
|
||||||
|
- 法规与安全:基于 Android 官方无障碍服务和企业微信官方 SDK,无 Hook / 无 Root / 无内存修改,注意遵守腾讯运营规范与相关法律法规。
|
||||||
|
|
||||||
|
更多面向使用方的说明及 API 文档,可参考:
|
||||||
|
|
||||||
|
- 项目 README:[README.md](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/README.md)
|
||||||
|
- 后端协议文档:[BACKEND_PROTOCOL.md](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/BACKEND_PROTOCOL.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 技术栈与运行环境
|
||||||
|
|
||||||
|
- 平台:Android
|
||||||
|
- 语言:Kotlin + Java 混合
|
||||||
|
- 构建工具:Gradle(使用仓库自带 `gradlew`)
|
||||||
|
- 最低支持版本:`minSdkVersion 24`
|
||||||
|
- 目标版本:`targetSdkVersion 30`
|
||||||
|
- 编译版本:`compileSdkVersion 30`
|
||||||
|
- Java 版本:`sourceCompatibility 1.8` / `targetCompatibility 1.8`
|
||||||
|
- IDE 建议:Android Studio(Arctic Fox 及以上版本均可正常打开)
|
||||||
|
|
||||||
|
涉及的关键第三方依赖(不完整列举):
|
||||||
|
|
||||||
|
- 工具库:`com.blankj:utilcodex`(通过 `com.blankj.utilcode.util.*` 等类使用)
|
||||||
|
- 日志:`LogUtils`(见 [LogUtilsInit.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/utils/LogUtilsInit.kt))
|
||||||
|
- Toast:`com.hjq:toast`(`ToastUtils`)
|
||||||
|
- UI 组件:`Material Components`(`MaterialAlertDialogBuilder`)、`QMUI` 对话框
|
||||||
|
- 网络通信:`OkHttp` WebSocket(见 [WebSocketManager.java](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/utils/WebSocketManager.java))
|
||||||
|
- 埋点统计:友盟 (`UMConfigure`),TalkingData (`TalkingDataSDK`)
|
||||||
|
- 企业微信 SDK:`lib_wwapi-*.aar`(位于 `app/libs/`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 工程结构总览
|
||||||
|
|
||||||
|
根目录主要文件与模块:
|
||||||
|
|
||||||
|
- `app/`:主 APP 工程(业务逻辑、UI、无障碍自动化、与后端通信等全部在此)
|
||||||
|
- `baselibrary/`:基础 UI / Adapter 等通用组件
|
||||||
|
- `floatwindow/`:悬浮窗相关实现
|
||||||
|
- `BACKEND_PROTOCOL.md`:客户端与后台 WebSocket 协议说明
|
||||||
|
- `README.md`:面向使用方的简要说明与版本更新记录
|
||||||
|
|
||||||
|
多模块声明见:
|
||||||
|
|
||||||
|
- [settings.gradle](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/settings.gradle)
|
||||||
|
- [app/build.gradle](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/build.gradle)
|
||||||
|
|
||||||
|
### 3.1 app 模块结构
|
||||||
|
|
||||||
|
路径:`app/src/main/java/org/yameida/worktool/`
|
||||||
|
|
||||||
|
主要包与职责:
|
||||||
|
|
||||||
|
- `activity/`
|
||||||
|
- `ListenActivity`:主控制面板,配置链接号(robotId)、后端 host、开关无障碍与悬浮窗,是日常运维主要入口。
|
||||||
|
- `LoginActivity`:登录相关界面。
|
||||||
|
- `SettingsActivity` / `SettingsAdvanceActivity`:基础及高级设置。
|
||||||
|
- `AccessibilityGuideActivity` / `FloatViewGuideActivity`:权限引导页面。
|
||||||
|
- `BrowserActivity`、`GetScreenShotActivity`:辅助操作页面。
|
||||||
|
- `service/`
|
||||||
|
- `WeworkService`:无障碍服务实现,负责监控和操作企业微信界面,是自动化的核心。
|
||||||
|
- `WeworkController`:统一调度控制,无障碍操作入口,管理循环任务状态。
|
||||||
|
- `WeworkLoopImpl` / `WeworkOperationImpl` / `WeworkInteractionImpl` / `WeworkGetImpl`:对企业微信不同操作的具体实现(循环拉取任务、执行发送消息、拉群、获取信息等)。
|
||||||
|
- `PlayNotifyService` / `PlayNotifyManager`:前台服务与通知控制,保障机器人在前台或长时间运行。
|
||||||
|
- `GlobalMethod.kt` / `MyLooper.kt`:封装通用操作和循环执行器。
|
||||||
|
- `utils/`
|
||||||
|
- 网络相关:`HttpUtil.kt`(REST 接口访问)、`OkHttpUtil.kt`、`WebSocketManager.java`。
|
||||||
|
- 无障碍与权限:`AccessibilityUtil.kt`、`AccessibilityExtraUtil.kt`、`PermissionHelper.kt`、`FlowPermissionHelper.kt`、`PermissionPageManagement.java`。
|
||||||
|
- 悬浮窗:`FloatWindowHelper.kt`,配合 `floatwindow` 模块实现全局悬浮按钮。
|
||||||
|
- 屏幕与截图:`capture/` 包下的 `ScreenCaptureUtil`、`ScreenCaptureUtilByMediaPro`、`MediaProjectionHolder.kt` 等。
|
||||||
|
- 配置与缓存:`CacheUtil.kt`、`PropUtil.kt`、`HostTestHelper.kt`。
|
||||||
|
- 其他工具:`RegexHelper.kt`、`RuntimeUtil.kt`、`WeworkRoomUtil.kt`、`WeworkTextUtil.kt` 等。
|
||||||
|
- `model/`
|
||||||
|
- `network/`:与后端通信的返回体,例如 `CheckUpdateResult.java`、`GetMyConfigResult.java`。
|
||||||
|
- `operation/`:执行结果封装,如 `SelectResult.kt`。
|
||||||
|
- `WeworkMessageBean.java` / `WeworkMessageListBean.kt`:WebSocket 消息封装,与 `BACKEND_PROTOCOL.md` 一一对应。
|
||||||
|
- `AppUpdate.java`、`MyConfigBean.kt` 等:应用更新 / 机器人配置相关模型。
|
||||||
|
- `notification/`:通知栏相关封装(`PlayNotifyManager.kt`)。
|
||||||
|
- `config/`:全局异常处理(`GlobalException.java`)。
|
||||||
|
- `observer/`:文件监听等辅助(`MultiFileObserver.java`)。
|
||||||
|
- 顶层文件:
|
||||||
|
- `MyApplication.kt`:全局 Application,完成 SDK 初始化、前台服务启动等。
|
||||||
|
- `Constant.kt`:全局常量与配置(可持久化到 `SPUtils`)。
|
||||||
|
- `Demo.kt`:示例或测试代码(如有使用可再确认)。
|
||||||
|
|
||||||
|
### 3.2 baselibrary 模块
|
||||||
|
|
||||||
|
路径:`baselibrary/`
|
||||||
|
|
||||||
|
- 主要保存通用 UI 组件与 Adapter,例如 `RvSimpleAdapter`、`RvViewHolder`。
|
||||||
|
- 资源文件中包含通用 `styles`、`strings` 等。
|
||||||
|
- 项目中作为 `app` 的依赖模块,无业务逻辑。
|
||||||
|
|
||||||
|
### 3.3 floatwindow 模块
|
||||||
|
|
||||||
|
路径:`floatwindow/`
|
||||||
|
|
||||||
|
- 浮窗管理:`FloatWindowManager.kt`、`BaseFloatWindow.java`、`DefaultFloatService.kt`。
|
||||||
|
- 监听器:`listener/` 包下 `FloatWindowListener.kt`、`OnClickListener.kt` 等。
|
||||||
|
- 视图:`view/HiderView.java` 与相关布局、图片资源。
|
||||||
|
- 为 `app` 提供悬浮窗入口、暂停 / 启动机器人等操作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 运行流程与关键逻辑
|
||||||
|
|
||||||
|
### 4.1 App 启动流程
|
||||||
|
|
||||||
|
入口类:
|
||||||
|
|
||||||
|
- `AndroidManifest.xml` 中配置 `MyApplication` 作为 `application`,主 Activity 为登录或 Listen 页(视具体配置而定)。
|
||||||
|
- [MyApplication.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/MyApplication.kt) 负责:
|
||||||
|
- 初始化 `Utils`、日志配置(`LogUtilsInit`)、`Gson` 代理。
|
||||||
|
- 初始化 `ToastUtils`。
|
||||||
|
- 初始化友盟埋点(`UMConfigure`),通过 `SPUtils` 中 `uminit` 标记隐私协议同意状态。
|
||||||
|
- 初始化 TalkingData(`TalkingDataSDK.init(...)`),`Constant.robotId` 作为渠道维度之一。
|
||||||
|
- 初始化企业微信 SDK(`IWWAPIUtil.init(this)`)。
|
||||||
|
- 初始化应用更新框架(`UpdateAppUtils.init(this)`)。
|
||||||
|
- 启动前台通知(`PlayNotifyManager.show()`)。
|
||||||
|
- 设置全局异常捕获(`GlobalException`),异常时自动重启。
|
||||||
|
|
||||||
|
### 4.2 ListenActivity 配置流程
|
||||||
|
|
||||||
|
核心界面:[ListenActivity.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/activity/ListenActivity.kt)。
|
||||||
|
|
||||||
|
主要职责:
|
||||||
|
|
||||||
|
- 展示并编辑链接号(`robotId`):
|
||||||
|
- 文本框 `et_channel` 显示当前 `Constant.robotId`。
|
||||||
|
- 点击“保存”按钮:
|
||||||
|
- 将输入写入 `Constant.robotId`(内部通过 `SPUtils` 持久化)。
|
||||||
|
- 发送广播通知 `Constant.WEWORK_NOTIFY`,类型为 `modify_channel`。
|
||||||
|
- 调用 `HttpUtil.getMyConfig(toast = false)` 拉取机器人配置。
|
||||||
|
- 将链接号上报给友盟 `MobclickAgent.onProfileSignIn(channel)`。
|
||||||
|
- 配置 WebSocket host:
|
||||||
|
- 文本 `tv_host` 显示 `Constant.host`。
|
||||||
|
- 点击:弹出历史 host 列表(来自 `SPUtils` 的 `host_list`),支持一键切换并调用 `HostTestHelper.testWs()` 进行连通性检测。
|
||||||
|
- 长按:弹出编辑对话框,支持新增 / 删除 host,新增时校验是否符合 `ws://` 或 `wss://` 开头格式。
|
||||||
|
- 权限开关:
|
||||||
|
- 无障碍开关 `sw_accessibility`:
|
||||||
|
- 校验是否填写链接号。
|
||||||
|
- 校验是否已开启系统无障碍;如未开启,跳转到引导页 `AccessibilityGuideActivity` 或系统设置。
|
||||||
|
- 如检测到 Hook / Root 环境,进行风险提示并控制运行。
|
||||||
|
- 关闭时调用 `WeworkController.weworkService.disableSelf()`(在部分 ROM 上会转跳到系统设置)。
|
||||||
|
- 悬浮窗开关 `sw_overlay`:
|
||||||
|
- 开启时跳转 `FloatViewGuideActivity` 引导授权。
|
||||||
|
- 已授权时显示悬浮窗(`FloatWindowHelper.showWindow()`)或引导前往权限设置页。
|
||||||
|
- 环境检测:
|
||||||
|
- 设备信息、Root 状态、Hook 状态写入 `SPUtils`,用于后续分析。
|
||||||
|
- 检查企业微信当前版本是否在 `Constant.AVAILABLE_VERSION` 列表内,给出“已适配 / 可能存在兼容性问题”等提示。
|
||||||
|
- 初始化数据:
|
||||||
|
- 在 `initData` 中设置默认 `Constant.robotId` 与 `Constant.host`(可以视需要调整或移除默认值)。
|
||||||
|
- 调用 `HttpUtil.getMyConfig(toast = false)` 拉取机器人配置。
|
||||||
|
- 调用 `CacheUtil.autoDelete()` 清理旧缓存。
|
||||||
|
|
||||||
|
### 4.3 与后端的通信
|
||||||
|
|
||||||
|
WebSocket 链接:
|
||||||
|
|
||||||
|
- WebSocket 地址统一由 `Constant.getWsUrl()` 生成,格式为:`<host>/webserver/wework/<robotId>`。
|
||||||
|
- 默认 host 在 `Constant.DEFAULT_HOST` 中配置:`ws://8.166.130.74:18680`。
|
||||||
|
- host 与 robotId 最终都持久化在 `SPUtils` 中,方便修改。
|
||||||
|
- 具体 WebSocket 管理由 [WebSocketManager.java](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/utils/WebSocketManager.java) 实现:
|
||||||
|
- 维护 WebSocket 连接、心跳(每 5 秒一次)、自动重连。
|
||||||
|
- 发送和确认消息(`WeworkMessageListBean` 封装),写入日志。
|
||||||
|
- 断线时关闭新消息接收(`WeworkController.setEnableLoopRunning(false)`),重连成功后再恢复。
|
||||||
|
- 当长时间连接正常且未开启截屏模式时,如果检测到长时间未重连且仍在运行,会通过 Toast 提示“机器人运行中 请勿人工操作手机~”。
|
||||||
|
|
||||||
|
协议格式:
|
||||||
|
|
||||||
|
- 完整协议说明见 [BACKEND_PROTOCOL.md](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/BACKEND_PROTOCOL.md):
|
||||||
|
- 连接 URL 格式、心跳机制。
|
||||||
|
- `WeworkMessageListBean`、`WeworkMessageBean` 的字段含义。
|
||||||
|
- 常用指令类型(如 `203` 发送消息、`218` 推送文件、`505` 获取最近聊天列表、`101` 接收消息列表等)。
|
||||||
|
- 执行结果回调、错误码定义。
|
||||||
|
- 代码中 `type`、`socketType` 等字段与文档一一对应,建议修改协议前同时修改此文档与对应模型类。
|
||||||
|
|
||||||
|
### 4.4 自动化执行流程(企业微信侧)
|
||||||
|
|
||||||
|
高层流程(简化):
|
||||||
|
|
||||||
|
1. 后端下发任务(通过 WebSocket)。
|
||||||
|
2. 客户端解析为 `WeworkMessageBean` / `WeworkMessageListBean`。
|
||||||
|
3. `WeworkController` 调度对应的 `Wework*Impl` 实现执行操作:
|
||||||
|
- 打开企业微信指定页面。
|
||||||
|
- 定位目标群 / 联系人。
|
||||||
|
- 根据控件树执行点击、输入文本、长按、转发等操作。
|
||||||
|
4. 执行完成后,将结果封装回 WebSocket 消息上报服务端,包含成功 / 失败列表、错误原因等。
|
||||||
|
|
||||||
|
关键依赖:
|
||||||
|
|
||||||
|
- 无障碍服务:`WeworkService` + `AccessibilityUtil` / `AccessibilityExtraUtil`。
|
||||||
|
- 文本匹配与房间识别:`RegexHelper`、`WeworkRoomUtil`、`WeworkTextUtil`。
|
||||||
|
- 循环任务:`MyLooper`、`WeworkLoopImpl`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置项与敏感信息
|
||||||
|
|
||||||
|
### 5.1 host 与 robotId
|
||||||
|
|
||||||
|
相关代码:
|
||||||
|
|
||||||
|
- [Constant.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/Constant.kt)
|
||||||
|
- [ListenActivity.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/activity/ListenActivity.kt)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `Constant.host`
|
||||||
|
- 默认值:`DEFAULT_HOST = "ws://8.166.130.74:18680"`。
|
||||||
|
- 通过 `SPUtils` 持久化,键为 `"host"`。
|
||||||
|
- 可在 ListenActivity 中通过 host 列表 / 编辑弹窗进行修改。
|
||||||
|
- 业务上可视为“后端 WebSocket 网关地址”,如需要更换环境(测试 / 生产)时重点关注。
|
||||||
|
- `Constant.robotId`
|
||||||
|
- 通过 ListenActivity 的输入框配置。
|
||||||
|
- 底层持久化键为 `"robotId"`,同时兼容历史 `"LISTEN_CHANNEL_ID"`。
|
||||||
|
- 后端一般将其视为“链接号 / 机器人唯一 ID”,用于区分不同设备 / 账号。
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
|
||||||
|
- 在更改默认 host 或引导用户填写 robotId 时,需要与后端保持一致(确保后台任务调度平台识别该 robotId)。
|
||||||
|
- 如需要支持多环境(测试 / 预发 / 生产),可以在现有 host 列表机制上扩展 UI 与配置管理。
|
||||||
|
|
||||||
|
### 5.2 企业微信兼容版本
|
||||||
|
|
||||||
|
相关代码:
|
||||||
|
|
||||||
|
- [Constant.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/Constant.kt) 中 `AVAILABLE_VERSION` 与 `AVAILABLE_VERSION_MAP`。
|
||||||
|
- ListenActivity 中通过 `AppUtils.getAppInfo(Constant.PACKAGE_NAMES)?.versionName` 获取企业微信版本。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `AVAILABLE_VERSION` 列出了当前已适配的企业微信版本号(例如 `4.1.8`、`4.1.9`、`4.1.10` 等)。
|
||||||
|
- `AVAILABLE_VERSION_MAP` 用于将版本号映射为内部的整数版本,用于兼容性判断。
|
||||||
|
- 当检测到企业微信版本不在列表内时,会提示“可能存在部分兼容性问题”,但仍允许使用。
|
||||||
|
|
||||||
|
后续维护建议:
|
||||||
|
|
||||||
|
- 当企业微信版本升级后,需要:
|
||||||
|
- 手动测试各项自动化功能。
|
||||||
|
- 若确认兼容,将新版本号加入 `AVAILABLE_VERSION` 与 `AVAILABLE_VERSION_MAP`。
|
||||||
|
- 如需要针对特定版本调整逻辑,可结合 `Constant.version` 做分支处理。
|
||||||
|
|
||||||
|
### 5.3 统计与第三方 SDK key
|
||||||
|
|
||||||
|
相关代码:
|
||||||
|
|
||||||
|
- [MyApplication.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/MyApplication.kt)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 友盟统计:
|
||||||
|
- `val key = "6284a3a3d024421570f97c3c"`
|
||||||
|
- 通过 `UMConfigure.preInit` / `UMConfigure.init` 初始化。
|
||||||
|
- 隐私协议同意状态通过 `SPUtils` 中 `"uminit"` 字段控制。
|
||||||
|
- TalkingData:
|
||||||
|
- `TalkingDataSDK.init(this, "80E9C84E39904DAFB28562910FF7C86C", getString(R.string.app_name) + "_master", Constant.robotId)`
|
||||||
|
- 依赖 `Constant.robotId` 作为渠道维度。
|
||||||
|
|
||||||
|
接手后如需更换统计账号或关闭统计:
|
||||||
|
|
||||||
|
- 调整或移除相应初始化代码。
|
||||||
|
- 同时评估对现有数据分析的影响,必要时与产品 / 运维同步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 构建、打包与发布流程
|
||||||
|
|
||||||
|
### 6.1 本地开发环境搭建
|
||||||
|
|
||||||
|
1. 克隆仓库到本地:
|
||||||
|
- 建议完整克隆含子模块(当前项目无 Git 子模块,仅多 module)。
|
||||||
|
2. 使用 Android Studio 打开仓库根目录。
|
||||||
|
3. 确保本地已安装:
|
||||||
|
- JDK 8(或 Android Studio 自带 JDK)。
|
||||||
|
- Android SDK 30 及对应 Build Tools。
|
||||||
|
4. 首次打开时,按 IDE 提示同步 Gradle,等待依赖下载完成。
|
||||||
|
|
||||||
|
### 6.2 常用构建命令
|
||||||
|
|
||||||
|
在项目根目录下,可以使用仓库自带 Gradle Wrapper:
|
||||||
|
|
||||||
|
- 编译 Debug 包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
- 编译 Release 包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- `app/build.gradle` 中 `release` 构建类型启用了混淆:
|
||||||
|
- `minifyEnabled true`
|
||||||
|
- 混淆配置文件:`proguard-android-optimize.txt`(默认)与 `app/proguard-rules.pro`
|
||||||
|
- 签名配置可能未在仓库中直接提供(如使用本地 `keystore`),如需正式发布包,请向原维护人员或运维团队索取签名文件与配置。
|
||||||
|
|
||||||
|
### 6.3 打包注意事项
|
||||||
|
|
||||||
|
- 由于集成了企业微信 SDK(`libs/lib_wwapi-*.aar`)及多个三方 SDK,混淆配置需要保持当前状态,避免将关键类混淆导致运行崩溃。
|
||||||
|
- 如新增依赖,请同步更新 `proguard-rules.pro`。
|
||||||
|
- Release 包上线前建议至少在以下维度自测:
|
||||||
|
- 不同 Android 版本(特别是 API 24~30)。
|
||||||
|
- 不同厂商 ROM(华为、小米、OPPO、VIVO 等)的无障碍与后台保活行为。
|
||||||
|
- 至少一种已适配的企业微信版本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 日常调试与常见问题
|
||||||
|
|
||||||
|
### 7.1 日志查看
|
||||||
|
|
||||||
|
- 项目统一使用 `LogUtils` 输出日志,初始化在 `LogUtilsInit` 中。
|
||||||
|
- 日志输出位置、格式可在 [LogUtilsInit.kt](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/app/src/main/java/org/yameida/worktool/utils/LogUtilsInit.kt) 中调整。
|
||||||
|
- 联调时建议:
|
||||||
|
- 在关键流程(WebSocket 收发、无障碍执行)增加必要的日志。
|
||||||
|
- 结合后端日志与手机本地日志定位问题。
|
||||||
|
|
||||||
|
### 7.2 无障碍与权限问题
|
||||||
|
|
||||||
|
常见现象:
|
||||||
|
|
||||||
|
- 无法自动跳转 / 点击:
|
||||||
|
- 检查系统无障碍设置中是否已为 WorkTool 授权。
|
||||||
|
- 检查是否被 ROM 的“电池优化 / 后台保护”策略限制。
|
||||||
|
- 悬浮窗不显示:
|
||||||
|
- 检查“悬浮窗权限”是否开启。
|
||||||
|
- 检查 `FloatWindowHelper.showWindow()` 是否被调用。
|
||||||
|
|
||||||
|
相关工具类:
|
||||||
|
|
||||||
|
- `PermissionHelper`:无障碍权限判断与相关操作。
|
||||||
|
- `FlowPermissionHelper`:后台启动、悬浮窗等权限判断。
|
||||||
|
- `PermissionPageManagement`:跳转至系统设置页面。
|
||||||
|
|
||||||
|
### 7.3 WebSocket 连接异常
|
||||||
|
|
||||||
|
排查步骤:
|
||||||
|
|
||||||
|
1. 在 ListenActivity 中确认 `host` 与 `robotId` 配置正确。
|
||||||
|
2. 使用 `HostTestHelper.testWs()`(ListenActivity 操作 host 时自动调用)测试连通性。
|
||||||
|
3. 查看 `WebSocketManager` 日志,确认是否存在频繁断线重连。
|
||||||
|
4. 确认服务端配置与 `BACKEND_PROTOCOL.md` 版本一致。
|
||||||
|
|
||||||
|
进一步建议(针对日志中偶发 `Connection closed` 场景):
|
||||||
|
|
||||||
|
- 现象判定:
|
||||||
|
- 若出现“断开后 1~2 秒内自动重连成功,且同一 clientId 可继续收发消息”,通常是短时网络抖动或网关空闲回收,不一定是严重故障。
|
||||||
|
- 若出现“周期性断开(如每几分钟一次)”或“重连失败持续告警”,优先排查配置问题。
|
||||||
|
- 归因方法(客户端 / 服务端 / 网关):
|
||||||
|
- 客户端(Android)补充日志:在 `WeworkService.EchoWebSocketListener` 的 `onClosing/onClosed/onFailure` 打印 `code`、`reason`、`throwable`。
|
||||||
|
- 服务端补充日志:在 WebSocket Handler 的关闭与异常回调中打印 `clientId`、`sessionId`、关闭码、异常栈。
|
||||||
|
- 网关层补充日志:Nginx / LB 记录 upstream 连接关闭原因与空闲超时触发记录。
|
||||||
|
- 通过同一时间窗口对齐三端日志,判断“谁先发起关闭、关闭码是什么、是否有网络异常”。
|
||||||
|
- 常见根因:
|
||||||
|
- 手机网络切换(Wi-Fi/移动网络)或弱网导致 TCP 断开。
|
||||||
|
- 网关 / 反向代理空闲超时过短,主动关闭 WebSocket。
|
||||||
|
- 心跳机制仅业务层保活,未启用 WebSocket ping,导致中间设备误回收长连接。
|
||||||
|
- Android 省电策略导致后台网络能力受限。
|
||||||
|
- 建议优化项(按优先级):
|
||||||
|
- 客户端统一复用 `OkHttpClient`,并开启 `pingInterval`(建议 20~30 秒)。
|
||||||
|
- 网关与服务端 idle timeout 应明显高于心跳周期(建议至少 30~60 秒)。
|
||||||
|
- 重连策略增加“指数退避 + 抖动”,避免批量设备同时重连。
|
||||||
|
- 在运维侧统计“每设备每小时断线次数、重连耗时、连续失败次数”,作为健康度指标。
|
||||||
|
|
||||||
|
相关代码入口:
|
||||||
|
|
||||||
|
- WebSocket 管理:`app/src/main/java/org/yameida/worktool/utils/WebSocketManager.java`
|
||||||
|
- 监听与回调:`app/src/main/java/org/yameida/worktool/service/WeworkService.kt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 后续可扩展与注意事项
|
||||||
|
|
||||||
|
- 协议变更:
|
||||||
|
- 修改 WebSocket 协议或新增指令类型时,需同时更新:
|
||||||
|
- `WeworkMessageBean` / `WeworkMessageListBean` 数据结构。
|
||||||
|
- `Wework*Impl` 中对新指令的处理逻辑。
|
||||||
|
- 文档 [BACKEND_PROTOCOL.md](file:///Users/tan/Documents/project/lzwcai-terminal-worktool/BACKEND_PROTOCOL.md)。
|
||||||
|
- 新企业微信版本适配:
|
||||||
|
- 新版本上线后,需实机测试高频功能。
|
||||||
|
- 确认后更新 `Constant.AVAILABLE_VERSION` 及相关逻辑。
|
||||||
|
- 安全与合规:
|
||||||
|
- 严禁在代码中硬编码任何生产环境账号密码。
|
||||||
|
- 涉及数据采集、上传时注意隐私合规(特别是好友信息、聊天内容等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 交接建议
|
||||||
|
|
||||||
|
建议接手同学优先按以下顺序熟悉项目:
|
||||||
|
|
||||||
|
1. 阅读 README 与本交接文档,理解整体定位与功能。
|
||||||
|
2. 在测试环境跑通完整链路:
|
||||||
|
- 编译安装 APP。
|
||||||
|
- 配置测试后端 host 与 robotId。
|
||||||
|
- 打开无障碍与悬浮窗权限。
|
||||||
|
- 使用后台调度平台下发几种典型任务(发送文本、发送图片、获取最近聊天列表等)。
|
||||||
|
3. 深入阅读核心代码:
|
||||||
|
- `MyApplication`(启动流程)。
|
||||||
|
- `ListenActivity`(配置入口)。
|
||||||
|
- `WeworkService` + `WeworkController` + `Wework*Impl`(自动化执行)。
|
||||||
|
- `WebSocketManager` + 协议模型类(通讯层)。
|
||||||
|
4. 根据实际需要再进一步梳理业务细节与扩展点。
|
||||||
|
|
||||||
|
如后续对具体模块或逻辑有疑问,可在对应类文件中逐步补充更细的内部文档或注释,并与团队同步更新本交接文档。
|
||||||
Reference in New Issue
Block a user