From bf36aa3303e652d8a04ad5c010e05c2e2fe10df5 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Wed, 29 Jul 2026 08:53:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(accessibility):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=97=A0=E9=9A=9C=E7=A2=8D=E6=9C=8D=E5=8A=A1=E8=BE=85=E5=8A=A9?= =?UTF-8?q?=E7=B1=BB=E5=B9=B6=E4=BC=98=E5=8C=96=E5=BE=AE=E4=BF=A1=E6=8B=A8?= =?UTF-8?q?=E5=8F=B7=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AccessibilityServiceHelper 类,提供无障碍服务启用状态检查、列表获取和状态设置功能 - 优化 DialerAccessibilityService 中的点击操作,移除模拟点击参数,统一使用节点点击方式 - 简化微信自动接听逻辑,合并多个相似方法为 findWithScroll 统一处理滚动查找 - 移除不再使用的回调接口和常量,精简代码结构 - 修复 MainActivity 中页面跳转逻辑,正确区分应用前后台切换状态 - 在设置页面集成无障碍服务开关控制,支持调试模式下直接启用服务 - 添加输入法隐藏功能,兼容不同安卓版本的键盘管理机制 - 优化节点查找算法,提升微信联系人搜索和拨号的稳定性 --- .../AccessibilityServiceHelper.java | 35 ++ .../DialerAccessibilityService.java | 523 +++++++++--------- .../dialer/activity/main/MainActivity.java | 29 +- .../settings/call/SettingsCallActivity.java | 27 +- .../ttstd/dialer/base/BaseApplication.java | 4 + .../dialer/base/BaseTransparentActivity.java | 3 - .../dialog/contact/call/CallFragment.java | 6 + .../com/ttstd/dialer/manager/AppManager.java | 52 ++ .../dialer/receiver/AppChangedReceiver.java | 11 + .../res/layout/activity_settings_call.xml | 8 + .../layout/layout_setting_content_item.xml | 16 +- app/src/main/res/values/strings.xml | 8 +- 12 files changed, 435 insertions(+), 287 deletions(-) diff --git a/app/src/main/java/com/ttstd/dialer/accessibility/AccessibilityServiceHelper.java b/app/src/main/java/com/ttstd/dialer/accessibility/AccessibilityServiceHelper.java index 3427ce6..575997c 100644 --- a/app/src/main/java/com/ttstd/dialer/accessibility/AccessibilityServiceHelper.java +++ b/app/src/main/java/com/ttstd/dialer/accessibility/AccessibilityServiceHelper.java @@ -6,10 +6,13 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.provider.Settings; +import android.text.TextUtils; import android.view.accessibility.AccessibilityManager; import com.ttstd.dialer.utils.Logger; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class AccessibilityServiceHelper { @@ -44,6 +47,38 @@ public class AccessibilityServiceHelper { return false; } + public static boolean isAdbAccessibilityServiceEnabled(Context context, Class serviceClass) { + List enabledServices = getEnabledAccessibilityServicesList(context); + String expectedServiceId = new ComponentName(context, serviceClass).flattenToShortString(); + return enabledServices.contains(expectedServiceId); + } + + public static List getEnabledAccessibilityServicesList(Context context) { + String enabledAccessibilityServices = Settings.Secure.getString(context.getContentResolver(), "enabled_accessibility_services"); + if (enabledAccessibilityServices != null) { + return new ArrayList<>(Arrays.asList(enabledAccessibilityServices.split(":"))); + } + return new ArrayList<>(); + } + + public static boolean setAccessibilityServiceStatus(Context context, Class serviceClass, boolean enable) { + List enabledServices = getEnabledAccessibilityServicesList(context); + String expectedServiceId = new ComponentName(context, serviceClass).flattenToShortString(); + if (enable) { + if (!enabledServices.contains(expectedServiceId)) { + enabledServices.add(expectedServiceId); + return Settings.Secure.putString(context.getContentResolver(), "enabled_accessibility_services", TextUtils.join(":", enabledServices)); + } + } else { + if (enabledServices.contains(expectedServiceId)) { + enabledServices.remove(expectedServiceId); + return Settings.Secure.putString(context.getContentResolver(), "enabled_accessibility_services", TextUtils.join(":", enabledServices)); + } + } + return isAdbAccessibilityServiceEnabled(context, serviceClass); + } + + /** * 跳转到系统的无障碍服务设置页面 * diff --git a/app/src/main/java/com/ttstd/dialer/accessibility/DialerAccessibilityService.java b/app/src/main/java/com/ttstd/dialer/accessibility/DialerAccessibilityService.java index ac91f1f..8af80a0 100644 --- a/app/src/main/java/com/ttstd/dialer/accessibility/DialerAccessibilityService.java +++ b/app/src/main/java/com/ttstd/dialer/accessibility/DialerAccessibilityService.java @@ -13,14 +13,19 @@ import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.DisplayMetrics; +import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; +import android.view.inputmethod.InputMethodManager; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import com.blankj.utilcode.util.ToastUtils; import com.kongzue.dialogx.dialogs.PopTip; @@ -31,13 +36,12 @@ import com.ttstd.dialer.utils.Logger; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Random; public class DialerAccessibilityService extends AccessibilityService { private static final String TAG = "AccessibilityService"; - private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + private final MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); public static final int ACTION_VIDEO = 1; public static final int ACTION_AUDIO = 2; @@ -49,7 +53,6 @@ public class DialerAccessibilityService extends AccessibilityService { private static final String CONTACT_TEXT = "通讯录"; private static final String SEARCH_TEXT = "搜索"; private static final String TAG_TEXT = "标签"; - private static final String MORE_NAME = "更多功能按钮,已折叠"; private static final String PARENT_VIDEO_TEXT = "视频通话"; private static final String VIDEO_TEXT = "视频通话"; @@ -68,6 +71,16 @@ public class DialerAccessibilityService extends AccessibilityService { private static final int MSG_DELAY_CLICK = 1002; private static final int MSG_RETRY_STEP = 1003; + private static final int MAX_FIND_COUNT = 5; + + public static final String SETTING_CALL_TYPE_ACTION = "setting_call_type_action"; + public static final String SETTING_AUTOMATIC_ANSWER_ACTION = "setting_automatic_answer_action"; + public static final String HIDE_IME_ACTION = "hide_ime_action"; + + //聊天页面的+号 + + private static final String MORE_FUNCTION_BUTTON_ID = "com.tencent.mm:id/bjz"; + private Handler mHandler; private Step mCurrentStep = Step.WAITING; private String mName = ""; @@ -79,15 +92,9 @@ public class DialerAccessibilityService extends AccessibilityService { private ContactInfo mContactInfo; private int mCallType = ACTION_VIDEO; private int mFindCount = 0; - private static final int MAX_FIND_COUNT = 5; private final Random mRandom = new Random(); - - public interface AccessibilityEventCallback { - void onAccessibilityEventCallback(AccessibilityEvent accessibilityEvent); - } - - private AccessibilityEventCallback mAccessibilityEventCallback; + private SettingReceiver mSettingReceiver; @Override public void onCreate() { @@ -97,7 +104,6 @@ public class DialerAccessibilityService extends AccessibilityService { registerSettingReceiver(); mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false); mAutoHandsFree = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_HNADS_FREE, false); - analysisAccessibilityEvent(); } private void initHandler() { @@ -110,7 +116,7 @@ public class DialerAccessibilityService extends AccessibilityService { break; case MSG_DELAY_CLICK: ClickInfo clickInfo = (ClickInfo) msg.obj; - performClick(clickInfo.node, clickInfo.simulate); + performClick(clickInfo.node); if (clickInfo.nextStep != null) { mCurrentStep = clickInfo.nextStep; sendProcessStepMessage(DEFAULT_DELAY); @@ -122,8 +128,7 @@ public class DialerAccessibilityService extends AccessibilityService { processCurrentStep(); } else { Logger.e(TAG, "Max retry count reached, reset to waiting"); - mFindCount = 0; - mCurrentStep = Step.WAITING; + resetToWaiting(); } break; } @@ -131,14 +136,19 @@ public class DialerAccessibilityService extends AccessibilityService { }; } + private void resetToWaiting() { + mFindCount = 0; + mCurrentStep = Step.WAITING; + } + private void sendProcessStepMessage(long delay) { mHandler.removeMessages(MSG_PROCESS_STEP); mHandler.sendEmptyMessageDelayed(MSG_PROCESS_STEP, delay); } - private void sendDelayClickMessage(AccessibilityNodeInfo node, boolean simulate, Step nextStep) { + private void sendDelayClickMessage(AccessibilityNodeInfo node, Step nextStep) { Message msg = mHandler.obtainMessage(MSG_DELAY_CLICK); - msg.obj = new ClickInfo(node, simulate, nextStep); + msg.obj = new ClickInfo(node, nextStep); long delay = DEFAULT_DELAY + mRandom.nextInt(RANDOM_DELAY_RANGE); mHandler.sendMessageDelayed(msg, delay); } @@ -148,9 +158,6 @@ public class DialerAccessibilityService extends AccessibilityService { mHandler.sendEmptyMessageDelayed(MSG_RETRY_STEP, delay); } - private void analysisAccessibilityEvent() { - } - @Override public int onStartCommand(Intent intent, int flags, int startId) { Logger.e(TAG, "onStartCommand: "); @@ -184,9 +191,6 @@ public class DialerAccessibilityService extends AccessibilityService { public void onAccessibilityEvent(AccessibilityEvent event) { Logger.v(TAG, "onAccessibilityEvent: event = " + event.toString()); checkClassName(event); - if (mAccessibilityEventCallback != null) { - mAccessibilityEventCallback.onAccessibilityEventCallback(event); - } if (mCurrentStep != Step.WAITING) { sendProcessStepMessage(100); } @@ -197,34 +201,33 @@ public class DialerAccessibilityService extends AccessibilityService { } + private void startWeixin() { + Intent intent = new Intent(); + ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI"); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.setComponent(cmp); + try { + startActivity(intent); + } catch (Exception e) { + Logger.e(TAG, "startWeixin: " + e.getMessage()); + } + } + private void checkClassName(AccessibilityEvent event) { Logger.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep); - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - String currentPackageName = event.getPackageName() != null ? event.getPackageName().toString() : ""; - String currentClassName = event.getClassName() != null ? event.getClassName().toString() : ""; - - switch (mCurrentStep) { - case WAITING: - if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) { - Logger.e(TAG, "checkClassName: to dialer hands free"); - } - break; - default: - if (!TextUtils.isEmpty(currentClassName)) { - switch (currentClassName) { - case "com.tencent.mm.ui.LauncherUI": - break; - case "com.tencent.mm.plugin.account.ui.WelcomeActivity": - case "com.tencent.mm.plugin.account.ui.LoginPasswordUI": - PopTip.show("请先登录微信").iconWarning(); - mCurrentStep = Step.WAITING; - break; - case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI": - break; - default: - } - } - } + if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + || mCurrentStep == Step.WAITING) { + return; + } + String currentClassName = event.getClassName() != null ? event.getClassName().toString() : ""; + if ("com.tencent.mm.plugin.account.ui.WelcomeActivity".equals(currentClassName) + || "com.tencent.mm.plugin.account.ui.LoginPasswordUI".equals(currentClassName)) { + PopTip.show("请先登录微信").iconWarning(); + mCurrentStep = Step.WAITING; } } @@ -266,7 +269,7 @@ public class DialerAccessibilityService extends AccessibilityService { putString(mName, Step.CLICK_SEARCH_CONTACT); break; case CLICK_SEARCH_CONTACT: - if (findNodesByViewId("com.tencent.mm:id/gzf").size() > 0) { + if (!findNodesByViewId("com.tencent.mm:id/gzf").isEmpty()) { findSearchContactAndClick("com.tencent.mm:id/odf", Step.CLICK_QUICK_WECHAT_CALL); } else { PopTip.show("没有找到联系人").iconError(); @@ -274,7 +277,7 @@ public class DialerAccessibilityService extends AccessibilityService { } break; case CLICK_QUICK_WECHAT_CALL: - clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET); + clickViewById(MORE_FUNCTION_BUTTON_ID, Step.CLICK_TARGET); break; case CLICK_TARGET: stepCall(Property.TEXT, PARENT_VIDEO_TEXT); @@ -292,7 +295,7 @@ public class DialerAccessibilityService extends AccessibilityService { } break; case FIND_CONTACT: - findContactWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); + findWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL, false); break; case FIND_TAG: findAndClick(Property.TEXT, TAG_TEXT, false, Step.CLICK_TAG); @@ -304,10 +307,10 @@ public class DialerAccessibilityService extends AccessibilityService { } break; case CLICK_NAME: - findContactWithScroll(Property.TEXT, mName, Step.CLICK_INFO); + findWithScroll(Property.TEXT, mName, Step.CLICK_INFO, false); break; case CLICK_INFO: - findAndClickWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL); + findWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL, true); break; default: } @@ -316,43 +319,37 @@ public class DialerAccessibilityService extends AccessibilityService { private boolean findAndClick(Property type, String text, boolean searchWindows, Step nextStep) { AccessibilityNodeInfo node = findNodeByProperty(type, text, searchWindows); if (node != null && isNodeVisible(node)) { - sendDelayClickMessage(node, false, nextStep); + sendDelayClickMessage(node, nextStep); return true; } return false; } - private void findAndClickWithScroll(Property type, String text, Step nextStep) { + /** + * 查找节点并点击,找不到时滚动重试。 + * + * @param checkVisible 为 true 时,找到但不可见的节点会先滚动屏幕后重试 + */ + private void findWithScroll(Property type, String text, Step nextStep, boolean checkVisible) { AccessibilityNodeInfo node = findNodeByProperty(type, text, false); if (node != null) { - if (node.isVisibleToUser()) { - sendDelayClickMessage(node, false, nextStep); + if (!checkVisible || node.isVisibleToUser()) { + sendDelayClickMessage(node, nextStep); mFindCount = 0; } else { scrollDown(); sendRetryStepMessage(WAIT_TIME); } } else { - handleNotFoundWithRetry("没有找到联系人", nextStep); + handleNotFoundWithRetry("没有找到联系人"); } } - private void findContactWithScroll(Property type, String text, Step nextStep) { - AccessibilityNodeInfo node = findNodeByProperty(type, text, false); - if (node != null) { - sendDelayClickMessage(node, false, nextStep); - mFindCount = 0; - } else { - handleNotFoundWithRetry("没有找到联系人", nextStep); - } - } - - private void handleNotFoundWithRetry(String errorMsg, Step nextStep) { + private void handleNotFoundWithRetry(String errorMsg) { if (mFindCount >= MAX_FIND_COUNT) { Logger.e(TAG, "handleNotFoundWithRetry: max count reached"); ToastUtils.showShort(errorMsg); - mCurrentStep = Step.WAITING; - mFindCount = 0; + resetToWaiting(); } else { Logger.e(TAG, "handleNotFoundWithRetry: not found, count=" + mFindCount); mFindCount++; @@ -364,26 +361,19 @@ public class DialerAccessibilityService extends AccessibilityService { private void handleHandsFree(Property type, String text, boolean isWechat) { AccessibilityNodeInfo node = findNodeByProperty(type, text, true); if (node != null) { - Point point = getPointByNode(node); - Logger.e(TAG, "handleHandsFree: " + point); if (isWechat) { - clickByPoint(point.x, point.y - 50); - clickByPoint(point.x, point.y); + doubleClickNodePoint(node); } else { - sendDelayClickMessage(node, false, Step.WAITING); + sendDelayClickMessage(node, Step.WAITING); } - mCurrentStep = Step.WAITING; } else { Logger.e(TAG, "handleHandsFree: not found"); - mCurrentStep = Step.WAITING; } + mCurrentStep = Step.WAITING; } private void autoAccept() { - if (findAndClickAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) { - mCurrentStep = Step.WECHAT_HANDS_FREE; - ToastUtils.showShort("已自动接听视频/语音"); - } else if (clickNode("com.tencent.mm:id/kfp", false)) { + if (findAndClickAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION) || clickNode("com.tencent.mm:id/kfp")) { mCurrentStep = Step.WECHAT_HANDS_FREE; ToastUtils.showShort("已自动接听视频/语音"); } else { @@ -394,48 +384,43 @@ public class DialerAccessibilityService extends AccessibilityService { private boolean findAndClickAnswer(Property type, String text) { AccessibilityNodeInfo node = findNodeByProperty(type, text, true); if (node != null) { - Point point = getPointByNode(node); - Logger.e(TAG, "findAndClickAnswer: " + point); - clickByPoint(point.x, point.y - 50); - clickByPoint(point.x, point.y); + doubleClickNodePoint(node); mCurrentStep = Step.WAITING; return true; } return false; } - private boolean clickNode(String id, boolean simulate) { - findFloatWindowNode(id); - List nodeInfos = findNodesByViewId(id); - Optional optional = nodeInfos.stream().findAny(); - if (optional.isPresent()) { - AccessibilityNodeInfo node = optional.get(); - if (node.isClickable()) { - boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); - Logger.e(TAG, "clickNode: performAction = " + performAction); - node.recycle(); - return performAction; - } else { - if (simulate) { - Point point = getPointByNode(node); - Logger.e(TAG, "clickNode: " + point); - clickByPoint(point.x, point.y); - Logger.e(TAG, "clickNode: mCurrentStep " + mCurrentStep + " done"); - } else { - AccessibilityNodeInfo clickableNode = findClickableNode(node); - if (clickableNode != null) { - sendDelayClickMessage(clickableNode, false, null); - } - } - } - return true; - } else { + /** + * 在节点中心点上方与中心点各点击一次(用于接听/免提按钮) + */ + private void doubleClickNodePoint(AccessibilityNodeInfo node) { + Point point = getPointByNode(node); + Logger.e(TAG, "doubleClickNodePoint: " + point); + clickByPoint(point.x, point.y - 50); + clickByPoint(point.x, point.y); + } + + private boolean clickNode(String id) { + AccessibilityNodeInfo node = findFirstNodeById(id); + if (node == null) { Logger.e(TAG, "clickNode: not found"); return false; } + if (node.isClickable()) { + boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + Logger.e(TAG, "clickNode: performAction = " + performAction); + node.recycle(); + return performAction; + } + AccessibilityNodeInfo clickableNode = findClickableNode(node); + if (clickableNode != null) { + sendDelayClickMessage(clickableNode, null); + } + return true; } - private void performClick(AccessibilityNodeInfo node, boolean simulate) { + private void performClick(AccessibilityNodeInfo node) { if (node == null) { Logger.e(TAG, "performClick: node is null"); return; @@ -446,26 +431,20 @@ public class DialerAccessibilityService extends AccessibilityService { } catch (Exception e) { Logger.e(TAG, "performClick: e = " + e.getMessage()); } - if (node.isClickable()) { - boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); - Logger.e(TAG, "performClick: performAction = " + performAction); - if (!performAction) { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Logger.e(TAG, "performClick: rect = " + rect); - int centerX = (rect.left + rect.right) / 2; - int centerY = (rect.top + rect.bottom) / 2; - Logger.e(TAG, "performClick: clickByNode = " + clickByPoint(centerX, centerY)); - } - node.recycle(); - } else { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Logger.e(TAG, "performClick: rect = " + rect); - int centerX = (rect.left + rect.right) / 2; - int centerY = (rect.top + rect.bottom) / 2; - Logger.e(TAG, "performClick: clickByNode = " + clickByPoint(centerX, centerY)); + boolean clickable = node.isClickable(); + boolean performed = false; + if (clickable) { + performed = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + Logger.e(TAG, "performClick: performAction = " + performed); } + if (!performed) { + Point point = getPointByNode(node); + Logger.e(TAG, "performClick: clickByNode = " + clickByPoint(point.x, point.y)); + } + if (clickable) { + node.recycle(); + } + hideInputMethod(); } private AccessibilityNodeInfo findClickableNode(AccessibilityNodeInfo node) { @@ -475,52 +454,24 @@ public class DialerAccessibilityService extends AccessibilityService { } if (node.isClickable()) { return node; - } else { - AccessibilityNodeInfo parent = node.getParent(); - node.recycle(); - return parent != null ? findClickableNode(parent) : null; - } - } - - public void findFloatWindowNode(String id) { - List windows = getWindows(); - for (AccessibilityWindowInfo window : windows) { - if (isFloatingWindow(window)) { - AccessibilityNodeInfo rootNode = window.getRoot(); - traverseNode(rootNode); - if (rootNode != null) { - rootNode.recycle(); - } - } - } - } - - private boolean isFloatingWindow(AccessibilityWindowInfo window) { - Logger.e(TAG, "isFloatingWindow: " + window.getType()); - return window.getType() == AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY; - } - - private void traverseNode(AccessibilityNodeInfo node) { - if (node == null) return; - String text = node.getText() != null ? node.getText().toString() : ""; - String id = node.getViewIdResourceName(); - for (int i = 0; i < node.getChildCount(); i++) { - traverseNode(node.getChild(i)); } + AccessibilityNodeInfo parent = node.getParent(); + node.recycle(); + return parent != null ? findClickableNode(parent) : null; } private AccessibilityNodeInfo findNodeByProperty(Property type, String text, boolean searchWindows) { if (searchWindows) { return findNodeInWindows(type, text); } else { - return findNodeInActiveWindow(getRootInActiveWindow(), type, text); + return findNodeInTree(getRootInActiveWindow(), type, text); } } private AccessibilityNodeInfo findNodeInWindows(Property type, String text) { for (AccessibilityWindowInfo window : getWindows()) { AccessibilityNodeInfo root = window.getRoot(); - AccessibilityNodeInfo node = findNodeInActiveWindow(root, type, text); + AccessibilityNodeInfo node = findNodeInTree(root, type, text); if (node != null) { return node; } @@ -532,20 +483,15 @@ public class DialerAccessibilityService extends AccessibilityService { return null; } - private AccessibilityNodeInfo findNodeInActiveWindow(AccessibilityNodeInfo root, Property type, String text) { + private AccessibilityNodeInfo findNodeInTree(AccessibilityNodeInfo root, Property type, String text) { if (root == null) return null; - Logger.v(TAG, "findNodeInActiveWindow: getText = " + root.getText()); - Logger.v(TAG, "findNodeInActiveWindow: getClassName = " + root.getClassName()); - Logger.v(TAG, "findNodeInActiveWindow: getContentDescription = " + root.getContentDescription()); - boolean satisfied = checkPropertyMatch(root, type, text); - if (satisfied) { + if (checkPropertyMatch(root, type, text)) { return root; - } else { - for (int i = 0; i < root.getChildCount(); i++) { - AccessibilityNodeInfo result = findNodeInActiveWindow(root.getChild(i), type, text); - if (result != null) { - return result; - } + } + for (int i = 0; i < root.getChildCount(); i++) { + AccessibilityNodeInfo result = findNodeInTree(root.getChild(i), type, text); + if (result != null) { + return result; } } root.recycle(); @@ -573,37 +519,38 @@ public class DialerAccessibilityService extends AccessibilityService { } private void clickViewById(String id, Step nextStep) { - List nodeInfos = findNodesByViewId(id); - Optional optional = nodeInfos.stream().findAny(); - if (optional.isPresent()) { - sendDelayClickMessage(optional.get(), false, nextStep); + AccessibilityNodeInfo node = findFirstNodeById(id); + if (node != null) { + sendDelayClickMessage(node, nextStep); } else { findAndClick(Property.DESCRIPTION, SEARCH_TEXT, false, Step.CLICK_SEARCH); } } + private AccessibilityNodeInfo findFirstNodeById(String id) { + List nodeInfos = findNodesByViewId(id); + return nodeInfos.isEmpty() ? null : nodeInfos.get(0); + } + private List findNodesByViewId(String id) { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { return nodeInfo.findAccessibilityNodeInfosByViewId(id); - } else { - return new ArrayList<>(); } + return new ArrayList<>(); } private void putString(String text, Step nextStep) { - List nodeInfos = findNodesByViewId("com.tencent.mm:id/d98"); - Optional optional = nodeInfos.stream().findAny(); - if (optional.isPresent()) { - AccessibilityNodeInfo nodeInfo = optional.get(); + AccessibilityNodeInfo nodeInfo = findFirstNodeById("com.tencent.mm:id/d98"); + if (nodeInfo != null) { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); - if (Build.VERSION.SDK_INT >= ACTION_IME_ENTER_VERSION) { nodeInfo.performAction(ACTION_IME_ENTER_ID); } + hideInputMethod(); mCurrentStep = nextStep; sendProcessStepMessage(WAIT_TIME); } else { @@ -613,31 +560,26 @@ public class DialerAccessibilityService extends AccessibilityService { } private void findSearchContactAndClick(String id, Step nextStep) { - List nodeInfos = findNodesByViewId(id); - Logger.e(TAG, "findSearchContactAndClick: " + nodeInfos); - Optional optional = nodeInfos.stream().findAny(); - if (optional.isPresent()) { - sendDelayClickMessage(optional.get(), false, nextStep); + AccessibilityNodeInfo node = findFirstNodeById(id); + if (node != null) { + sendDelayClickMessage(node, nextStep); } else { PopTip.show("没有找到联系人").iconError(); mCurrentStep = Step.WAITING; } } - private boolean stepCall(Property type, String text) { + private void stepCall(Property type, String text) { AccessibilityNodeInfo node = findNodeByProperty(type, text, false); if (node != null) { Point point = getPointByNode(node); Logger.e(TAG, "stepCall: " + point); clickByPoint(point.x, point.y); - Logger.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done"); mCurrentStep = Step.CLICK_CALL; Logger.e(TAG, "stepCall: next " + mCurrentStep); sendProcessStepMessage(WAIT_TIME); - return true; } else { Logger.e(TAG, "stepCall: not found"); - return false; } } @@ -662,62 +604,43 @@ public class DialerAccessibilityService extends AccessibilityService { } private boolean clickByPoint(int x, int y) { - Logger.e(TAG, "clickByPoint: x = " + x); - Logger.e(TAG, "clickByPoint: y = " + y); - Point point = new Point(x, y); + Logger.e(TAG, "clickByPoint: x = " + x + ", y = " + y); Path path = new Path(); - path.moveTo(point.x, point.y); - GestureDescription.Builder builder = new GestureDescription.Builder(); + path.moveTo(x, y); int duration = 200 + mRandom.nextInt(100); - builder.addStroke(new GestureDescription.StrokeDescription(path, 0, duration)); - GestureDescription gesture = builder.build(); - boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { - @Override - public void onCompleted(GestureDescription gestureDescription) { - super.onCompleted(gestureDescription); - Logger.e("clickByPoint", "onCompleted: "); - } - - @Override - public void onCancelled(GestureDescription gestureDescription) { - super.onCancelled(gestureDescription); - Logger.e("clickByPoint", "onCancelled: "); - } - }, null); - return dispatched; + return dispatchGesturePath(path, duration, "clickByPoint"); } private boolean scrollScreen(double startY, double endY) { WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getRealMetrics(dm); - int width = dm.widthPixels; - int height = dm.heightPixels; - Logger.e(TAG, "scrollScreen: screenWidth = " + width); - Logger.e(TAG, "scrollScreen: screenHeight = " + height); - int center_X = width / 2; - int center_Y = height / 2; - Logger.e("scrollScreen", "center position:" + "(" + center_X + "," + center_Y + ")"); + int centerX = dm.widthPixels / 2; + int centerY = dm.heightPixels / 2; + Logger.e(TAG, "scrollScreen: center position: (" + centerX + "," + centerY + ")"); Path path = new Path(); - path.moveTo(center_X, (int) (center_Y * startY)); - path.lineTo(center_X, (int) (center_Y * endY)); - GestureDescription.Builder builder = new GestureDescription.Builder(); - GestureDescription gestureDescription = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build(); - boolean dispatched = dispatchGesture(gestureDescription, new GestureResultCallback() { + path.moveTo(centerX, (int) (centerY * startY)); + path.lineTo(centerX, (int) (centerY * endY)); + return dispatchGesturePath(path, 200, "scrollScreen"); + } + + private boolean dispatchGesturePath(Path path, int duration, String tag) { + GestureDescription gesture = new GestureDescription.Builder() + .addStroke(new GestureDescription.StrokeDescription(path, 0, duration)) + .build(); + return dispatchGesture(gesture, new GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { super.onCompleted(gestureDescription); - Logger.d("scrollScreen", "dispatchGesture ScrollUp onCompleted."); - path.close(); + Logger.e(tag, "onCompleted: "); } @Override public void onCancelled(GestureDescription gestureDescription) { super.onCancelled(gestureDescription); - Logger.d("scrollScreen", "dispatchGesture ScrollUp cancel."); + Logger.e(tag, "onCancelled: "); } }, null); - return dispatched; } private boolean scrollDown() { @@ -728,6 +651,87 @@ public class DialerAccessibilityService extends AccessibilityService { return scrollScreen(0.5, 1.5); } + /** + * 隐藏输入法(软键盘),兼容各安卓版本。 + * 优先通过 InputMethodManager 直接隐藏;若不支持或失败,则模拟返回键收起键盘。 + */ + public void hideInputMethod() { + if (!isInputMethodShown()) { + Logger.e(TAG, "hideInputMethod: 输入法未显示,无需隐藏"); + return; + } + if (!hideInputMethodByImm() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // 兜底方案:输入法显示时返回键只会收起键盘,不会退出当前页面 + performGlobalAction(GLOBAL_ACTION_BACK); + } + } + + /** + * 判断输入法(软键盘)是否正在显示。 + * Android 5.0(API 21)及以上可通过无障碍窗口类型精确判断; + * 低版本无法可靠判断,默认返回 true 以尝试隐藏。 + */ + private boolean isInputMethodShown() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + List windows = getWindows(); + if (windows != null) { + for (AccessibilityWindowInfo window : windows) { + if (window.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { + return true; + } + } + } + return false; + } + return true; + } + + /** + * 通过反射调用 InputMethodManager.hideSoftInputFromWindow 直接隐藏输入法,无返回键副作用。 + * 依次尝试从 IMM 内部不同版本的字段中获取当前窗口 token。 + */ + private boolean hideInputMethodByImm() { + try { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return false; + } + IBinder token = getInputMethodWindowToken(imm); + if (token == null) { + Logger.e(TAG, "hideInputMethodByImm: 未获取到窗口 token"); + return false; + } + Method hideMethod = InputMethodManager.class.getMethod("hideSoftInputFromWindow", IBinder.class, int.class); + return (Boolean) hideMethod.invoke(imm, token, 0); + } catch (Exception e) { + Logger.e(TAG, "hideInputMethodByImm: " + e.getMessage()); + return false; + } + } + + /** + * 反射获取 InputMethodManager 持有的当前窗口 token(不同安卓版本字段名不同)。 + */ + private IBinder getInputMethodWindowToken(InputMethodManager imm) { + String[] tokenFieldNames = {"mCurRootView", "mServedView", "mNextServedView"}; + for (String fieldName : tokenFieldNames) { + try { + Field field = imm.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Object view = field.get(imm); + if (view instanceof View) { + IBinder token = ((View) view).getWindowToken(); + if (token != null) { + return token; + } + } + } catch (Exception ignored) { + // 尝试下一个字段名 + } + } + return null; + } + private enum Step { WAITING, WECHAT_HANDS_FREE, @@ -743,8 +747,7 @@ public class DialerAccessibilityService extends AccessibilityService { FIND_TAG, CLICK_TAG, CLICK_NAME, - CLICK_INFO, - CLICK_VIDEO_CALL + CLICK_INFO } private enum Property { @@ -754,22 +757,15 @@ public class DialerAccessibilityService extends AccessibilityService { } private static class ClickInfo { - AccessibilityNodeInfo node; - boolean simulate; - Step nextStep; + final AccessibilityNodeInfo node; + final Step nextStep; - ClickInfo(AccessibilityNodeInfo node, boolean simulate, Step nextStep) { + ClickInfo(AccessibilityNodeInfo node, Step nextStep) { this.node = node; - this.simulate = simulate; this.nextStep = nextStep; } } - public static final String SETTING_CALL_TYPE_ACTION = "setting_call_type_action"; - public static final String SETTING_AUTOMATIC_ANSWER_ACTION = "setting_automatic_answer_action"; - - private SettingReceiver mSettingReceiver; - private void registerSettingReceiver() { if (mSettingReceiver == null) { mSettingReceiver = new SettingReceiver(); @@ -777,7 +773,12 @@ public class DialerAccessibilityService extends AccessibilityService { IntentFilter filter = new IntentFilter(); filter.addAction(SETTING_CALL_TYPE_ACTION); filter.addAction(SETTING_AUTOMATIC_ANSWER_ACTION); - registerReceiver(mSettingReceiver, filter); + filter.addAction(HIDE_IME_ACTION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mSettingReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(mSettingReceiver, filter); + } } private class SettingReceiver extends BroadcastReceiver { @@ -788,33 +789,19 @@ public class DialerAccessibilityService extends AccessibilityService { if (TextUtils.isEmpty(action)) return; switch (action) { case SETTING_CALL_TYPE_ACTION: - int callType = intent.getIntExtra("call_type", ACTION_VIDEO); - mCallType = callType; - Logger.e("SettingReceiver", "onReceive: callType = " + callType); + mCallType = intent.getIntExtra("call_type", ACTION_VIDEO); + Logger.e("SettingReceiver", "onReceive: callType = " + mCallType); break; case SETTING_AUTOMATIC_ANSWER_ACTION: - boolean autoAnswer = intent.getBooleanExtra("auto_answer", false); - mAutoAccept = autoAnswer; - Logger.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer); + mAutoAccept = intent.getBooleanExtra("auto_answer", false); + Logger.e("SettingReceiver", "onReceive: autoAnswer = " + mAutoAccept); + break; + case HIDE_IME_ACTION: + hideInputMethod(); break; default: } } } - private void startWeixin() { - Intent intent = new Intent(); - ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI"); - intent.setAction(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.setComponent(cmp); - try { - startActivity(intent); - } catch (Exception e) { - Logger.e(TAG, "startWeixin: " + e.getMessage()); - } - } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java index 8488403..89dabda 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -60,6 +60,14 @@ public class MainActivity extends BaseMvvmActivity mAppInfos; @@ -261,14 +269,18 @@ public class MainActivity extends BaseMvvmActivity { private static final String TAG = "SettingsActivity"; @@ -33,7 +39,24 @@ public class SettingsCallActivity extends BaseMvvmActivity= Build.VERSION_CODES.O) { + V26Helper.refreshAppLabels(this); + } else { + ASYNC_EXECUTOR.execute(this::refreshAppLabelsSync); + } + } + + private void refreshAppLabelsSync() { + try { + List allApps = getAllDesktopSortApps(); + if (allApps.isEmpty()) return; + + boolean changed = false; + for (AppInfo app : allApps) { + if (app.getComponentName() == null) continue; + String newLabel = ApkUtils.getAppName(mContext, app.getComponentName()); + if (!Objects.equals(newLabel, app.getLabel())) { + app.setLabel(newLabel); + try { + mAppRepository.update(app); + changed = true; + } catch (Exception e) { + Logger.e(TAG, "刷新应用名称失败: " + app.getPackageName(), e); + } + } + } + + if (changed) { + Logger.i(TAG, "系统语言切换,应用名称刷新完成"); + LiveDataBus.get().send(LiveDataAction.ACTION_UPDATE_APPS, TAG); + } + } catch (Exception e) { + Logger.e(TAG, "刷新应用名称时发生异常", e); + } + } + public void refreshAllApps() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { V26Helper.refreshAllApps(this); @@ -917,6 +959,16 @@ public class AppManager { }, ASYNC_EXECUTOR); } + @SuppressLint("NewApi") + static void refreshAppLabels(final AppManager manager) { + CompletableFuture.runAsync(new Runnable() { + @Override + public void run() { + manager.refreshAppLabelsSync(); + } + }, ASYNC_EXECUTOR); + } + @SuppressLint("NewApi") static void refreshAllApps(final AppManager manager) { CompletableFuture.runAsync(new Runnable() { diff --git a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java index ff9899d..31001c8 100644 --- a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java +++ b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java @@ -29,6 +29,17 @@ public class AppChangedReceiver extends BroadcastReceiver { if (TextUtils.isEmpty(action)) { return; } + // 系统语言切换,刷新所有应用名称(该广播无 package data) + if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { + sExecutor.execute(() -> { + try { + AppManager.getInstance().refreshAppLabels(); + } catch (Exception e) { + Log.e(TAG, "onReceive: refreshAppLabels " + e.getMessage()); + } + }); + return; + } String packageName = intent.getDataString().replace("package:", ""); switch (action) { case Intent.ACTION_PACKAGE_ADDED: diff --git a/app/src/main/res/layout/activity_settings_call.xml b/app/src/main/res/layout/activity_settings_call.xml index ff7f5b9..12ff237 100644 --- a/app/src/main/res/layout/activity_settings_call.xml +++ b/app/src/main/res/layout/activity_settings_call.xml @@ -82,6 +82,14 @@ android:background="@drawable/settings_card_bg" android:orientation="vertical"> + + 使用拨号助手一键拨号 - 开启快捷通话 + 快捷通话 已开启,快捷联系人通话类型 未开启,快捷联系人通话类型 @@ -23,7 +23,7 @@ 已开启,自动接听视频和语音 未开启,自动接听视频和语音 - 自动开启免提 + 自动打开免提 已开启,自动开启免提 未开启,自动开启免提 @@ -33,7 +33,7 @@ - 开启悬浮按钮 + 全局悬浮按钮 已开启,点小圆点可以直接返回桌面 未开启,点小圆点可以直接返回桌面 @@ -45,7 +45,7 @@ 已设置为默认桌面 未设置为默认桌面 - 开启整点报时 + 整点报时 已开启,整点时将自动语音报时 未开启,整点时将自动语音报时