diff --git a/app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java b/app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java index 018301b..5ebd415 100644 --- a/app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java +++ b/app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.graphics.Path; import android.graphics.Rect; import android.os.Build; +import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.view.accessibility.AccessibilityEvent; @@ -42,6 +43,12 @@ public class AblService extends AccessibilityService { private Disposable mdDisposable; private String TAG = "commongendan"; public String SHEBEITIPE_VILUETHREE = "3"; + + // 钉钉打卡相关 + private static final String DINGDING_PACKAGE = "com.alibaba.android.rimet"; + private boolean isDingDingDakaStarted = false; // 是否已启动钉钉打卡流程 + private int dingDingDakaStep = 0; // 打卡步骤:0-未开始,1-等待启动,2-点击首页打卡,3-点击打卡页面打卡 + private Handler dingDingHandler = new Handler(); // 钉钉打卡专用 Handler public static AblService getInstance() { if (mAblService == null) { @@ -147,7 +154,14 @@ public class AblService extends AccessibilityService { //当窗口状态发生改变的时候 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: String classname = event.getClassName().toString(); - Log.e("TIAOSHI", "有弹出==22222" + "==classname==" + classname); + String packageName = event.getPackageName() != null ? event.getPackageName().toString() : ""; + Log.e("TIAOSHI", "有弹出==22222" + "==classname==" + classname + ", packageName=" + packageName); + + // 检测钉钉应用窗口,处理打卡流程 + if (DINGDING_PACKAGE.equals(packageName) && isDingDingDakaStarted) { + handleDingDingDakaWindow(classname); + } + List text = event.getText(); if (text.size() > 0) { CharSequence charSequence = text.get(0); @@ -613,4 +627,187 @@ public class AblService extends AccessibilityService { }, null); } + /** + * 启动钉钉打卡流程 + * 由 MainActivity 调用 + */ + public void startDingDingDaka() { + Log.e("TIAOSHI###", "AblService: 启动钉钉打卡流程"); + isDingDingDakaStarted = true; + dingDingDakaStep = 1; // 步骤1:等待应用启动 + // 延迟2秒后执行步骤2 + dingDingHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (isDingDingDakaStarted && dingDingDakaStep == 1) { + Log.e("TIAOSHI###", "AblService: 钉钉打卡-步骤1完成,开始步骤2"); + dingDingDakaStep = 2; + executeDingDingDakaStep2(); + } + } + }, 2000); + } + + /** + * 处理钉钉应用窗口状态变化 + */ + private void handleDingDingDakaWindow(String className) { + Log.e("TIAOSHI###", "AblService: 检测到钉钉窗口变化,className=" + className + ", step=" + dingDingDakaStep); + // 如果正在执行打卡流程,根据步骤执行相应操作 + if (dingDingDakaStep == 2) { + // 步骤2:在首页查找并点击打卡按钮 + executeDingDingDakaStep2(); + } else if (dingDingDakaStep == 3) { + // 步骤3:在打卡页面点击打卡按钮 + executeDingDingDakaStep3(); + } + } + + /** + * 执行步骤2:在钉钉首页查找并点击"打卡"按钮 + */ + private void executeDingDingDakaStep2() { + Log.e("TIAOSHI###", "AblService: 钉钉打卡-步骤2: 查找并点击打卡按钮"); + if (clickDakaButton("打卡")) { + Log.e("TIAOSHI###", "AblService: 成功点击打卡按钮,等待跳转到打卡页面"); + dingDingDakaStep = 3; + // 延迟2秒后执行步骤3,等待页面跳转完成 + dingDingHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (isDingDingDakaStarted && dingDingDakaStep == 3) { + Log.e("TIAOSHI###", "AblService: 钉钉打卡-步骤2完成,开始步骤3"); + executeDingDingDakaStep3(); + } + } + }, 2000); + } else { + // 尝试其他可能的文本 + Log.e("TIAOSHI###", "AblService: 未找到'打卡',尝试其他文本"); + if (clickDakaButton("考勤打卡") || clickDakaButton("工作") || clickDakaButton("工作台")) { + Log.e("TIAOSHI###", "AblService: 找到并点击了其他入口"); + dingDingDakaStep = 3; + dingDingHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (isDingDingDakaStarted && dingDingDakaStep == 3) { + executeDingDingDakaStep3(); + } + } + }, 2000); + } else { + Log.e("TIAOSHI###", "AblService: 未找到打卡入口,结束流程"); + resetDingDingDaka(); + } + } + } + + /** + * 执行步骤3:在打卡页面查找并点击"打卡"按钮 + */ + private void executeDingDingDakaStep3() { + Log.e("TIAOSHI###", "AblService: 钉钉打卡-步骤3: 在打卡页面点击打卡"); + // if (clickDakaButton("打卡")) { + // Log.e("TIAOSHI###", "AblService: 成功完成打卡操作"); + // resetDingDingDaka(); + // } else { + // // 尝试其他可能的打卡按钮文本 + // if (clickDakaButton("上班打卡") || clickDakaButton("下班打卡") || + // clickDakaButton("外勤打卡") || clickDakaButton("立即打卡")) { + // Log.e("TIAOSHI###", "AblService: 成功完成打卡操作"); + // resetDingDingDaka(); + // } else { + // Log.e("TIAOSHI###", "AblService: 未找到打卡按钮,结束流程"); + // resetDingDingDaka(); + // } + // } + } + + /** + * 查找并点击包含指定文本的按钮 + */ + private boolean clickDakaButton(String text) { + try { + AccessibilityNodeInfo root = getRootInActiveWindow(); + if (root == null) { + Log.e("TIAOSHI###", "AblService: 无法获取根节点"); + return false; + } + + // 遍历所有节点查找文本 + return findAndClickDakaNode(root, text); + + } catch (Exception e) { + Log.e("TIAOSHI###", "AblService: 查找打卡按钮异常: " + text, e); + return false; + } + } + + /** + * 递归查找并点击包含指定文本的节点 + */ + private boolean findAndClickDakaNode(AccessibilityNodeInfo node, String text) { + if (node == null) { + return false; + } + + try { + // 检查当前节点的文本和描述 + CharSequence nodeText = node.getText(); + CharSequence nodeContentDesc = node.getContentDescription(); + + // 输出节点信息用于调试 + if (nodeText != null || nodeContentDesc != null) { + Log.e("TIAOSHI###", "AblService: 节点: " + node.getClassName() + " 文本: " + nodeText + " 描述: " + nodeContentDesc); + } + + // 检查是否包含目标文本 + if ((nodeText != null && nodeText.toString().contains(text)) || + (nodeContentDesc != null && nodeContentDesc.toString().contains(text))) { + + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + if (rect.width() > 0 && rect.height() > 0 && node.isEnabled()) { + Log.e("TIAOSHI###", "AblService: 找到匹配节点: " + text + " 坐标: [" + rect.left + "," + rect.top + "][" + rect.right + "," + rect.bottom + "]"); + + // 尝试点击节点 + if (node.isClickable()) { + boolean clicked = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + if (clicked) { + Log.e("TIAOSHI###", "AblService: 成功点击节点: " + text); + return true; + } + } else { + // 如果节点不可点击,使用 performViewClick(会自动向上查找可点击的父节点) + AblViewUtil.performViewClick(node); + Log.e("TIAOSHI###", "AblService: 使用 performViewClick 点击: " + text); + return true; + } + } + } + + // 递归查找子节点 + for (int i = 0; i < node.getChildCount(); i++) { + AccessibilityNodeInfo child = node.getChild(i); + if (findAndClickDakaNode(child, text)) { + return true; + } + } + } catch (Exception e) { + Log.w("TIAOSHI###", "AblService: 查找节点异常", e); + } + + return false; + } + + /** + * 重置钉钉打卡状态 + */ + private void resetDingDingDaka() { + Log.e("TIAOSHI###", "AblService: 重置钉钉打卡状态"); + isDingDingDakaStarted = false; + dingDingDakaStep = 0; + dingDingHandler.removeCallbacksAndMessages(null); // 清除所有延迟任务 + } + } diff --git a/app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java b/app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java index ca49177..dc12c36 100644 --- a/app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java +++ b/app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java @@ -638,15 +638,18 @@ public class MainActivity extends BaseActivity implements InfoMessage { } // 启动自动化打卡流程 - // 延迟1秒后发送 STEP_270 消息,让钉钉应用有时间启动 + // 延迟1秒后启动打卡流程,让钉钉应用有时间启动 new Handler().postDelayed(new Runnable() { @Override public void run() { Log.e("TIAOSHI###", "MainActivity: 启动钉钉打卡自动化流程"); - // 必须先设置 setStop(false),否则消息不会被处理 - AblStepHandler.getInstance().setStop(false); - Log.e("TIAOSHI###", "MainActivity: 已设置 setStop(false),发送 STEP_270 消息"); - AblStepHandler.sendMsg(AblSteps.STEP_270); + try { + AblService.getInstance().startDingDingDaka(); + Log.e("TIAOSHI###", "MainActivity: 已调用 AblService.startDingDingDaka()"); + } catch (Exception e) { + Log.e("MainActivity", "启动钉钉打卡流程失败", e); + SafeToastUtils.showShort(MainActivity.this, "启动打卡流程失败:" + e.getMessage()); + } } }, 1000); } catch (Exception e) { diff --git a/钉钉打卡.txt b/钉钉打卡.txt new file mode 100644 index 0000000..dfd2c68 --- /dev/null +++ b/钉钉打卡.txt @@ -0,0 +1,167 @@ +钉钉打卡功能实现文档 +==================== + +一、功能概述 +----------- +实现了钉钉应用的自动化打卡功能,通过无障碍服务(AccessibilityService)自动完成以下操作: +1. 启动钉钉应用 +2. 在钉钉首页查找并点击"打卡"按钮 +3. 等待跳转到打卡页面 +4. 在打卡页面点击"打卡"按钮完成打卡 + +二、实现方案 +----------- +将钉钉打卡的业务逻辑直接集成到 AblService(无障碍服务)中,而不是使用 AblStepHandler。 +这样做的优势: +- 避免 Handler 消息丢失:业务逻辑在无障碍服务中执行,即使主应用在后台也能正常运行 +- 更可靠:无障碍服务是系统级服务,不会被轻易暂停 +- 代码集中:所有打卡逻辑都在 AblService 中,便于维护 + +三、修改文件清单 +--------------- + +1. app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java +2. app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java + +四、详细修改说明 +--------------- + +【文件1】AblService.java + +1. 添加导入 + - import android.os.Handler; + +2. 添加成员变量(第47-51行) + ```java + // 钉钉打卡相关 + private static final String DINGDING_PACKAGE = "com.alibaba.android.rimet"; + private boolean isDingDingDakaStarted = false; // 是否已启动钉钉打卡流程 + private int dingDingDakaStep = 0; // 打卡步骤:0-未开始,1-等待启动,2-点击首页打卡,3-点击打卡页面打卡 + private Handler dingDingHandler = new Handler(); // 钉钉打卡专用 Handler + ``` + +3. 修改 onAccessibilityEvent 方法(第155-162行) + 在 TYPE_WINDOW_STATE_CHANGED 事件处理中添加钉钉窗口检测: + ```java + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + String classname = event.getClassName().toString(); + String packageName = event.getPackageName() != null ? event.getPackageName().toString() : ""; + Log.e("TIAOSHI", "有弹出==22222" + "==classname==" + classname + ", packageName=" + packageName); + + // 检测钉钉应用窗口,处理打卡流程 + if (DINGDING_PACKAGE.equals(packageName) && isDingDingDakaStarted) { + handleDingDingDakaWindow(classname); + } + ``` + +4. 添加核心方法 + + a) startDingDingDaka() - 启动打卡流程(第634-649行) + - 由 MainActivity 调用 + - 设置 isDingDingDakaStarted = true + - 设置 dingDingDakaStep = 1(等待应用启动) + - 延迟2秒后执行步骤2 + + b) handleDingDingDakaWindow() - 处理钉钉窗口状态变化(第654-664行) + - 当检测到钉钉应用窗口变化时调用 + - 根据当前步骤执行相应操作 + + c) executeDingDingDakaStep2() - 执行步骤2:点击首页打卡按钮(第669-703行) + - 查找并点击包含"打卡"文本的按钮 + - 如果未找到,尝试"考勤打卡"、"工作"、"工作台" + - 点击成功后,延迟2秒执行步骤3 + + d) executeDingDingDakaStep3() - 执行步骤3:点击打卡页面打卡按钮(第708-724行) + - 当前已注释,等待用户根据实际需求实现 + - 可以查找并点击"打卡"、"上班打卡"、"下班打卡"、"外勤打卡"、"立即打卡"等按钮 + + e) clickDakaButton() - 查找并点击包含指定文本的按钮(第729-744行) + - 获取根节点 + - 调用 findAndClickDakaNode 递归查找 + + f) findAndClickDakaNode() - 递归查找并点击节点(第749-801行) + - 检查节点的文本和描述是否包含目标文本 + - 如果节点可点击,直接点击 + - 如果节点不可点击,使用 AblViewUtil.performViewClick() 自动查找可点击的父节点 + - 递归查找子节点 + + g) resetDingDingDaka() - 重置打卡状态(第806-811行) + - 重置所有状态变量 + - 清除所有延迟任务 + +【文件2】MainActivity.java + +1. 修改 executeDingDingDaka() 方法(第640-654行) + 将原来的 AblStepHandler 调用改为直接调用 AblService 的方法: + + 原代码: + ```java + AblStepHandler.getInstance().setStop(false); + AblStepHandler.sendMsg(AblSteps.STEP_270); + ``` + + 新代码: + ```java + AblService.getInstance().startDingDingDaka(); + ``` + +五、执行流程 +----------- + +1. 用户在 MainActivity 中点击"钉钉打卡"按钮 +2. MainActivity.executeDingDingDaka() 被调用 +3. 检查钉钉应用是否安装(使用多种方法:getPackageInfo、getLaunchIntentForPackage、queryIntentActivities) +4. 启动钉钉应用(优先使用 getLaunchIntentForPackage,失败则使用 LaunchHomeActivity) +5. 延迟1秒后调用 AblService.startDingDingDaka() +6. AblService 设置 isDingDingDakaStarted = true,dingDingDakaStep = 1 +7. 延迟2秒后执行步骤2(executeDingDingDakaStep2) +8. 在钉钉首页查找并点击"打卡"按钮 +9. 点击成功后,设置 dingDingDakaStep = 3,延迟2秒后执行步骤3 +10. 在打卡页面查找并点击"打卡"按钮(当前已注释,需要根据实际需求实现) +11. 打卡完成后调用 resetDingDingDaka() 重置状态 + +六、关键日志标识 +--------------- +所有钉钉打卡相关的日志都使用 "TIAOSHI###" 作为标识,便于过滤和调试: +- "AblService: 启动钉钉打卡流程" +- "AblService: 钉钉打卡-步骤1完成,开始步骤2" +- "AblService: 钉钉打卡-步骤2: 查找并点击打卡按钮" +- "AblService: 钉钉打卡-步骤3: 在打卡页面点击打卡" +- "AblService: 找到匹配节点: ..." +- "AblService: 成功点击节点: ..." + +七、注意事项 +----------- + +1. 无障碍服务必须已开启,否则 AblService.getInstance() 会抛出异常 +2. 钉钉应用的包名是 "com.alibaba.android.rimet" +3. 步骤3(executeDingDingDakaStep3)当前已注释,需要根据实际打卡页面的UI实现 +4. 所有延迟时间(2秒)可以根据实际情况调整 +5. 如果找不到打卡按钮,会尝试多种可能的文本,如果都找不到则结束流程 + +八、待完善功能 +----------- + +1. executeDingDingDakaStep3() 方法当前已注释,需要根据实际打卡页面的UI实现 +2. 可以添加更多的错误处理和重试机制 +3. 可以添加打卡结果的通知或回调 + +九、测试建议 +----------- + +1. 确保无障碍服务已开启 +2. 确保钉钉应用已安装 +3. 在 MainActivity 中点击"钉钉打卡"按钮 +4. 观察日志输出,确认每个步骤是否正常执行 +5. 如果某个步骤失败,检查日志中的节点信息,调整查找逻辑 + +十、相关文件 +----------- + +- app/src/main/java/com/fisherbone/fuzhu/abllib/AblService.java +- app/src/main/java/com/fisherbone/fuzhu/activity/MainActivity.java +- app/src/main/java/com/fisherbone/fuzhu/abllib/utils/AblViewUtil.java(提供 performViewClick 方法) +- app/src/main/res/layout/activity_main.xml(包含"钉钉打卡"UI元素) + +--- +文档生成时间:2026-01-06