11 Commits

Author SHA1 Message Date
57cf264e44 fix(企业微信服务): 修复未读聊天检测与点击逻辑
调整变量命名以准确反映收集的是聊天行节点而非红点节点
新增两种点击方式的兜底逻辑,提升点击成功率
添加页面跳转验证,避免点击未生效的假成功情况
优化异常处理流程,点击失败时直接返回false
2026-05-11 16:29:11 +08:00
tanjianbin
9bfb1a9040 update 2026-05-11 16:25:47 +08:00
cad16fa7ca fix(WeworkService): 修复重连后无法检测新消息的问题
重连成功时恢复主循环运行状态并重新发送循环消息,避免服务假运行
2026-05-11 16:19:01 +08:00
2ae2ab0ff5 chore: 移除废弃的打印节点功能及无用设置菜单项
1. 移除左右悬浮菜单中的打印节点按钮及相关点击逻辑
2. 调整悬浮菜单截图按钮的布局间距与logo图标位置,修复XML文件末尾换行问题
3. 删除设置页面的检查更新、赞助、分享应用菜单项及对应代码
2026-05-11 16:08:15 +08:00
ccc0c3c4ae fix(accessibility, floatwindow): 同步无障碍服务状态并优化悬浮窗控制逻辑
- 新增无障碍服务状态广播,实现服务启停状态与UI页面同步
- 重置悬浮窗暂停状态,避免服务重启后卡在上次暂停状态
- 为悬浮窗菜单添加播放/暂停和停止功能
- 修复ListenActivity中无障碍开关切换的无限循环问题
2026-05-11 15:53:42 +08:00
9a9ffb79d5 fix bug 2026-05-11 15:41:50 +08:00
1d9a77a112 bug 2026-05-11 14:59:40 +08:00
8efa4889be websocket 2026-05-11 14:58:31 +08:00
81615a27d9 reconnect 2026-05-11 14:52:26 +08:00
473e2c2d89 change 2026-05-11 14:32:27 +08:00
c6fbcec2d6 fix(WeworkTextUtil): 改进文件大小格式检测以支持更多常见格式
改进正则表达式以兼容更多常见文件大小格式,如"12M"、"12MB"、"12.5 MB"、"12 kb"等。同时添加空值检查,避免空字符串或null值导致异常。
2026-05-11 11:41:45 +08:00
17 changed files with 277 additions and 414 deletions

View File

@@ -58,7 +58,7 @@ object Constant {
SPUtils.getInstance().put(weworkCorpName + "weworkMP", value)
}
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
get() = SPUtils.getInstance().getBoolean("groupStrict", false)
set(value) = SPUtils.getInstance().put("groupStrict", value)

View File

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

View File

@@ -22,11 +22,13 @@ import org.yameida.worktool.utils.*
import org.yameida.worktool.utils.capture.MediaProjectionHolder
import org.yameida.worktool.utils.envcheck.CheckHook
import org.yameida.worktool.utils.envcheck.CheckRoot
import kotlin.random.Random
class ListenActivity : AppCompatActivity() {
var riskRetry: Int = 0
private var updatingAccessibilitySwitch = false
companion object {
/**
@@ -59,12 +61,13 @@ class ListenActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(openWsReceiver)
FloatWindowHelper.hideWindow()
}
override fun onResume() {
super.onResume()
sw_overlay.isChecked = Settings.canDrawOverlays(Utils.getApp()) && FlowPermissionHelper.canBackgroundStart(Utils.getApp())
sw_accessibility.isChecked = PermissionHelper.isAccessibilitySettingOn()
refreshAccessibilitySwitch()
if (needToWork) {
needToWork = false
goToWork()
@@ -75,16 +78,11 @@ class ListenActivity : AppCompatActivity() {
iv_settings.setOnClickListener {
SettingsActivity.enterActivity(this)
}
et_channel.setText(Constant.robotId)
bt_save.setOnClickListener {
val channel = et_channel.text.toString().trim()
Constant.robotId = channel
ToastUtils.showLong("保存成功")
sendBroadcast(Intent(Constant.WEWORK_NOTIFY).apply {
putExtra("type", "modify_channel")
})
HttpUtil.getMyConfig(toast = false)
MobclickAgent.onProfileSignIn(channel)
tv_channel.text = Constant.robotId.ifBlank { "未生成" }
bt_reset_channel.setOnClickListener {
val channel = generateDefaultRobotId()
tv_channel.text = channel
saveChannel(channel, toast = "重置成功")
}
tv_host.text = Constant.host
tv_host.setOnClickListener {
@@ -131,6 +129,9 @@ class ListenActivity : AppCompatActivity() {
private fun initAccessibility() {
sw_accessibility.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
if (updatingAccessibilitySwitch) {
return@OnCheckedChangeListener
}
LogUtils.i("sw_accessibility onCheckedChanged: $isChecked")
if (isChecked) {
if (Constant.robotId.isBlank()) {
@@ -185,13 +186,45 @@ class ListenActivity : AppCompatActivity() {
}
private fun initData() {
Constant.robotId = "2038521871751249921"
Constant.host = "ws://8.166.130.74:18680"
// 链接号为空时自动生成一次并持久化,避免每次启动覆盖用户手动保存的链接号
if (Constant.robotId.isBlank()) {
Constant.robotId = generateDefaultRobotId()
tv_channel.text = Constant.robotId
}
// HttpUtil.checkUpdate()
HttpUtil.getMyConfig(toast = false)
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() {
if (!Constant.enableMediaProject) {
return
@@ -206,7 +239,7 @@ class ListenActivity : AppCompatActivity() {
}, Context.BIND_AUTO_CREATE)
//开启屏幕录制权限
if (MediaProjectionHolder.mMediaProjection == null) {
bt_save.postDelayed({
window.decorView.postDelayed({
fastStartActivity(this, GetScreenShotActivity::class.java)
}, 1000)
}
@@ -282,8 +315,8 @@ class ListenActivity : AppCompatActivity() {
.setNegativeButton("", null)
.setPositiveButton("", null)
val show = positiveButton.show()
bt_save.postDelayed({ show.dismiss() }, 5000)
bt_save.postDelayed({
window.decorView.postDelayed({ show.dismiss() }, 5000)
window.decorView.postDelayed({
packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(this)
@@ -293,10 +326,21 @@ class ListenActivity : AppCompatActivity() {
private val openWsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getStringExtra("type") == "openWs") {
needToWork = intent.getBooleanExtra("switch", false)
when (intent.getStringExtra("type")) {
"openWs" -> {
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
}
}

View File

@@ -69,9 +69,6 @@ class SettingsActivity : AppCompatActivity() {
})
rl_reply_strategy.setOnClickListener { showReplyStrategyDialog() }
rl_log.setOnClickListener { showLogDialog() }
rl_update.setOnClickListener { showUpdateDialog() }
rl_donate.setOnClickListener { showDonateDialog() }
rl_share.setOnClickListener { showShareDialog() }
rl_advance.setOnClickListener { SettingsAdvanceActivity.enterActivity(this) }
freshOpenFlow()
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() {
if (Settings.canDrawOverlays(Utils.getApp())) {
if (FlowPermissionHelper.canBackgroundStart(Utils.getApp())) {

View File

@@ -1,7 +1,6 @@
package org.yameida.worktool.service
import com.blankj.utilcode.util.*
import org.yameida.worktool.Demo
import org.yameida.worktool.annotation.RequestMapping
import org.yameida.worktool.model.ExecCallbackBean
import org.yameida.worktool.model.WeworkMessageBean
@@ -18,6 +17,8 @@ object WeworkController {
/** 是否正在等待回复 - 等待时会暂停主循环扫描新消息 */
var waitingForReply = false
fun isServiceReady(): Boolean = ::weworkService.isInitialized
/**
* 交互通知
* @see WeworkMessageBean.TYPE_CONSOLE_TOAST
@@ -159,7 +160,6 @@ object WeworkController {
@RequestMapping
fun test(message: WeworkMessageBean? = null) {
LogUtils.d(message)
Demo.test(true)
}
/**

View File

@@ -7,7 +7,6 @@ import androidx.core.text.isDigitsOnly
import com.blankj.utilcode.util.*
import com.hjq.toast.ToastUtils
import org.yameida.worktool.Constant
import org.yameida.worktool.Demo
import org.yameida.worktool.MyApplication
import org.yameida.worktool.activity.GetScreenShotActivity
import org.yameida.worktool.model.WeworkMessageBean
@@ -176,8 +175,7 @@ object WeworkLoopImpl {
val nameList = passFriendRequest()
if (nameList.isEmpty())
break
//todo 可自定义执行任务
// Demo.test2(nameList[0])
// todo 可自定义执行任务
}
}
return true
@@ -638,7 +636,7 @@ object WeworkLoopImpl {
* 检查首页-聊天列表是否有未读红点并点击进入
*/
private fun checkUnreadChatRoom(list: AccessibilityNodeInfo): Boolean {
val spotNodeList = arrayListOf<AccessibilityNodeInfo>()
val unreadRowNodeList = arrayListOf<AccessibilityNodeInfo>()
for (i in 0 until list.childCount) {
val item = list.getChild(i)
if (item != null && Views.RelativeLayout.equals(item.className)) {
@@ -649,20 +647,22 @@ object WeworkLoopImpl {
&& spotNode.text != null
&& spotNode.text.toString().replace("+", "").isDigitsOnly()
) {
spotNodeList.add(spotNode)
unreadRowNodeList.add(item)
}
}
}
}
if (spotNodeList.size > 0) {
LogUtils.i("发现未读消息: " + spotNodeList.size + "")
log("发现未读消息: " + spotNodeList.size + "")
if (AccessibilityUtil.performClick(spotNodeList.firstOrNull())) {
//进入聊天页 下一步 getChatMessageList
} else {
AccessibilityUtil.clickByNode(WeworkController.weworkService, spotNodeList.firstOrNull()?.parent)
if (unreadRowNodeList.isNotEmpty()) {
LogUtils.i("发现未读消息: " + unreadRowNodeList.size + "")
log("发现未读消息: " + unreadRowNodeList.size + "")
val firstUnreadRow = unreadRowNodeList.firstOrNull()
val clicked = AccessibilityUtil.performClick(firstUnreadRow)
|| AccessibilityUtil.clickByNode(WeworkController.weworkService, firstUnreadRow)
if (!clicked) {
return false
}
return true
// 避免“点击红点但未实际进会话”的假成功
return AccessibilityUtil.waitForPageMissing("WwMainActivity", "GlobalSearchActivity", timeout = 2000)
} else {
return false
}

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.FileObserver
import android.os.Message
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import com.blankj.utilcode.util.*
@@ -13,11 +14,10 @@ import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
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.utils.*
import java.lang.Exception
import kotlin.concurrent.thread
/**
* 企业微信辅助服务
@@ -39,24 +39,23 @@ class WeworkService : AccessibilityService() {
LogUtils.i("初始化成功")
//隐藏软键盘模式
softKeyboardController.showMode = SHOW_MODE_HIDDEN
// 服务重启后恢复主功能默认运行态,避免被上一次暂停状态卡住
FloatWindowHelper.isPause = false
WeworkController.weworkService = this
WeworkController.enableLoopRunning = true
notifyAccessibilityState(true)
//初始化长连接
initWebSocket()
//初始化消息处理器
MyLooper.init()
//初始化图片接收
initObserver()
//开发者可以在这里添加测试代码 启动时调用一次
thread { Demo.test(AppUtils.isAppDebug()) }
//监听是否修改链接号并重新长连接
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getStringExtra("type") == "modify_channel") {
LogUtils.e("更新channel")
webSocketManager.close(1000, "modify_channel")
initWebSocket()
reconnectWebSocket("modify_channel")
}
}
}, IntentFilter(Constant.WEWORK_NOTIFY))
@@ -69,6 +68,14 @@ class WeworkService : AccessibilityService() {
webSocketManager = WebSocketManager(url, listener)
}
fun reconnectWebSocket(reason: String) {
LogUtils.i("reconnectWebSocket: $reason")
if (::webSocketManager.isInitialized) {
webSocketManager.close(1000, reason)
}
initWebSocket()
}
private fun initObserver() {
if (!Constant.pushImage) return
try {
@@ -113,16 +120,24 @@ class WeworkService : AccessibilityService() {
LogUtils.i("onDestroy")
//关闭自动回复
WeworkController.enableLoopRunning = false
// 关闭主功能时清理暂停态,确保下次开启可立即进入检测
FloatWindowHelper.isPause = false
notifyAccessibilityState(false)
//隐藏软键盘模式
softKeyboardController.showMode = SHOW_MODE_AUTO
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() {
private val TAG = "WeworkService.EchoWebSocketListener"
private lateinit var socket: WebSocket
override fun onOpen(webSocket: WebSocket, response: Response) {
socket = webSocket
Log.e(TAG, "连接建立")
val robotId = Constant.robotId
val appVersion = SPUtils.getInstance().getString("appVersion", "")
@@ -131,6 +146,13 @@ class WeworkService : AccessibilityService() {
val hook = SPUtils.getInstance().getBoolean("hook", false)
LogUtils.i("连接建立: $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("设置自动跳转企业微信")
sendBroadcast(true)
}
@@ -154,7 +176,7 @@ class WeworkService : AccessibilityService() {
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
socket.close(code, reason)
webSocket.close(code, reason)
Log.e(TAG, "服务端关闭连接 $code: $reason")
sendBroadcast(false)
}

View File

@@ -31,6 +31,7 @@ import kotlin.concurrent.thread
object FloatWindowHelper {
var isPause = false
private var bound = false
fun showWindow() {
LogUtils.d("FloatWindowHelper.showWindow()")
@@ -39,7 +40,23 @@ object FloatWindowHelper {
val app = Utils.getApp()
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 {
override fun onClick(v: View, event: Int) {
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 -> {
if (PermissionHelper.isAccessibilitySettingOn()) {
if (isPause) {
@@ -127,6 +115,28 @@ object FloatWindowHelper {
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 -> {
Utils.getApp().packageManager.getLaunchIntentForPackage(Constant.PACKAGE_NAMES)?.apply {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -143,6 +153,7 @@ object FloatWindowHelper {
override fun onServiceDisconnected(name: ComponentName?) {
LogUtils.i("DefaultFloatService 服务断开")
bound = false
}
}

View File

@@ -112,8 +112,10 @@ object HttpUtil {
}
override fun onError(response: Response<String>) {
ToastUtils.showLong("获取配置失败 请检查机器人ID")
LogUtils.e("获取配置失败 请检查机器人ID")
if (toast) {
ToastUtils.showLong("获取配置失败请检查网络/Host/机器人ID")
}
LogUtils.e("获取配置失败: ${response.code()} ${response.exception?.message}")
}
})
}

View File

@@ -22,32 +22,37 @@ import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class WebSocketManager {
public static final String HEARTBEAT = "{\"type\":" + WeworkMessageBean.HEART_BEAT + "}";
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<>();
private static final int reconnectInt = 5000; //毫秒
private static final long heartBeatRate = 5; //秒
private Map<String, Long> messageIdMap = new ConcurrentHashMap<>();
private ScheduledFuture task;
private WebSocket socket;
private String url;
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;
public WebSocketManager(String url, WebSocketListener listener) {
Log.e(url, "新建链接");
this.url = url;
this.listener = listener;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
this.socket = client.newWebSocket(request, listener);
socket.send("{\"td\":" + System.currentTimeMillis() + "}");
this.socket = client.newWebSocket(new Request.Builder().url(url).build(), innerListener);
webSocketManager.put(url, this);
task = heartCheckStart();
}
@@ -83,7 +88,7 @@ public class WebSocketManager {
public void send(WeworkMessageListBean msg, boolean log) {
String json = GsonUtils.toJson(msg);
boolean success = socket.send(json);
boolean success = socket != null && socket.send(json);
if (log && success)
LogUtils.d(url, json, "通讯消息发送成功!");
if (!success)
@@ -91,14 +96,19 @@ public class WebSocketManager {
}
public void send(String msg) {
boolean success = socket.send(msg);
boolean success = socket != null && socket.send(msg);
LogUtils.e(url, msg, (success ? "通讯消息发送成功!" : "通讯消息发送失败!"));
}
public void close(int code, String reason) {
task.cancel(true);
manuallyClosed = true;
if (task != null) {
task.cancel(true);
}
Log.e("url", "task 取消");
this.socket.close(code, reason);
if (this.socket != null) {
this.socket.close(code, reason);
}
Log.e(url, "链接关闭");
}
@@ -112,44 +122,25 @@ public class WebSocketManager {
}
public void reConnect() {
if (manuallyClosed || connecting) {
return;
}
connecting = true;
opened = false;
Log.e(url, "重连");
boolean isConnect = false;
int interval = reconnectInt;
while (true) {
try {
isConnect = connect();
if (isConnect) {
connecting = false;
break;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
Thread.sleep(interval);
if (interval < 600000) {
interval *= 2;
}
} catch (InterruptedException e) {
e.printStackTrace();
try {
if (socket != null) {
socket.cancel();
}
} catch (Exception ignore) {
}
}
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;
socket = client.newWebSocket(new Request.Builder().url(url).build(), innerListener);
}
private ScheduledFuture heartCheckStart() {
lastConnectedTime = System.currentTimeMillis();
Runnable r = () -> {
if (manuallyClosed) return;
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
Log.d(url, "心跳检测" + df.format(new Date()));// new Date()为获取当前系统时间
if (!connecting && (socket == null || !socket.send(HEARTBEAT))) {
@@ -159,7 +150,7 @@ public class WebSocketManager {
reConnect();
//重连后刷新连接时间
lastConnectedTime = System.currentTimeMillis();
} else if (System.currentTimeMillis() % 1000 == 0) {
} else if (opened && System.currentTimeMillis() % 1000 == 0) {
socket.send("{\"td\":" + System.currentTimeMillis() + "}");
}
if (!Constant.INSTANCE.getEnableMediaProject()) {
@@ -176,4 +167,46 @@ public class WebSocketManager {
public static WebSocketManager getWebSocketManager(String 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);
}
};
}

View File

@@ -329,7 +329,12 @@ object WeworkTextUtil {
* 是否为文件上方时间
*/
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)
)
}
/**

View File

@@ -326,11 +326,14 @@
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<EditText
android:id="@+id/et_channel"
<TextView
android:id="@+id/tv_channel"
android:layout_width="match_parent"
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:textSize="@dimen/setting_end_font_size" />
@@ -344,17 +347,17 @@
android:orientation="vertical">
<Button
android:id="@+id/bt_save"
android:id="@+id/bt_reset_channel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/comment_red_btn"
android:paddingStart="50dp"
android:paddingEnd="50dp"
android:textSize="18sp"
android:textStyle="bold"
android:paddingStart="42dp"
android:paddingEnd="42dp"
android:text="重置"
android:textColor="@color/white"
android:text="保存" />
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>

View File

@@ -473,172 +473,6 @@
</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
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -56,7 +56,14 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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)
}
@@ -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) {
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) {
onClickListener?.onClick(v,2)
}
@@ -105,7 +109,6 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
leftView = inflater.inflate(R.layout.layout_menu_left, null)
leftView.iv_logo_left.setOnClickListener(this)
leftView.iv_logo_left2.setOnClickListener(this)
leftView.iv_start_left.setOnClickListener(this)
leftView.iv_shot_left.setOnClickListener(this)
leftView.iv_back_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.iv_logo_right.setOnClickListener(this)
rightView.iv_logo_right2.setOnClickListener(this)
rightView.iv_start_right.setOnClickListener(this)
rightView.iv_shot_right.setOnClickListener(this)
rightView.iv_back_right.setOnClickListener(this)
rightView.iv_resume_pause_right.setOnClickListener(this)
@@ -197,4 +199,10 @@ class DefaultFloatService : BaseFloatWindow(), View.OnClickListener {
}
override fun onDestroyed() {}
override fun onDestroy() {
hide()
stopForeground(true)
super.onDestroy()
}
}

View File

@@ -11,9 +11,13 @@ object FloatWindowManager {
private val TAG = FloatWindowManager::class.java.simpleName
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) {
startServiceSafe(Intent(context, service).apply {
putExtra(EXTRA_ACTION, ACTION_SHOW)
if (intent != null) {
this.putExtras(intent)
}
@@ -22,6 +26,7 @@ object FloatWindowManager {
fun hide(service: Class<out BaseFloatWindow>, intent: Intent? = null) {
startServiceSafe(Intent(context, service).apply {
putExtra(EXTRA_ACTION, ACTION_HIDE)
if (intent != null) {
this.putExtras(intent)
}

View File

@@ -16,19 +16,12 @@
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
android:id="@+id/iv_shot_left"
android:layout_width="@dimen/float_size"
android:layout_height="@dimen/float_size"
android:layout_marginStart="@dimen/float_margin_start"
android:layout_marginTop="29dp"
android:layout_marginTop="0dp"
android:src="@drawable/float_icon_pause" />
<ImageView
@@ -44,7 +37,7 @@
android:id="@+id/iv_logo_left"
android:layout_width="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_marginTop="10dp"
android:layout_marginEnd="55dp"

View File

@@ -15,18 +15,11 @@
android:layout_height="190dp"
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
android:id="@+id/iv_shot_right"
android:layout_width="@dimen/float_size"
android:layout_height="@dimen/float_size"
android:layout_marginTop="29dp"
android:layout_marginTop="0dp"
android:src="@drawable/float_icon_pause" />
<ImageView
@@ -41,7 +34,7 @@
android:id="@+id/iv_logo_right"
android:layout_width="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_marginTop="10dp"
android:src="@mipmap/ic_launcher_round" />