refactor(accessibility): 重构无障碍服务辅助类并优化微信拨号逻辑

- 新增 AccessibilityServiceHelper 类,提供无障碍服务启用状态检查、列表获取和状态设置功能
- 优化 DialerAccessibilityService 中的点击操作,移除模拟点击参数,统一使用节点点击方式
- 简化微信自动接听逻辑,合并多个相似方法为 findWithScroll 统一处理滚动查找
- 移除不再使用的回调接口和常量,精简代码结构
- 修复 MainActivity 中页面跳转逻辑,正确区分应用前后台切换状态
- 在设置页面集成无障碍服务开关控制,支持调试模式下直接启用服务
- 添加输入法隐藏功能,兼容不同安卓版本的键盘管理机制
- 优化节点查找算法,提升微信联系人搜索和拨号的稳定性
This commit is contained in:
2026-07-29 08:53:01 +08:00
parent 5a3eb31625
commit bf36aa3303
12 changed files with 435 additions and 287 deletions

View File

@@ -6,10 +6,13 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
public class AccessibilityServiceHelper { public class AccessibilityServiceHelper {
@@ -44,6 +47,38 @@ public class AccessibilityServiceHelper {
return false; return false;
} }
public static boolean isAdbAccessibilityServiceEnabled(Context context, Class<?> serviceClass) {
List<String> enabledServices = getEnabledAccessibilityServicesList(context);
String expectedServiceId = new ComponentName(context, serviceClass).flattenToShortString();
return enabledServices.contains(expectedServiceId);
}
public static List<String> 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<String> 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);
}
/** /**
* 跳转到系统的无障碍服务设置页面 * 跳转到系统的无障碍服务设置页面
* *

View File

@@ -13,14 +13,19 @@ import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo; 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.blankj.utilcode.util.ToastUtils;
import com.kongzue.dialogx.dialogs.PopTip; import com.kongzue.dialogx.dialogs.PopTip;
@@ -31,13 +36,12 @@ import com.ttstd.dialer.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Random; import java.util.Random;
public class DialerAccessibilityService extends AccessibilityService { public class DialerAccessibilityService extends AccessibilityService {
private static final String TAG = "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_VIDEO = 1;
public static final int ACTION_AUDIO = 2; 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 CONTACT_TEXT = "通讯录";
private static final String SEARCH_TEXT = "搜索"; private static final String SEARCH_TEXT = "搜索";
private static final String TAG_TEXT = "标签"; private static final String TAG_TEXT = "标签";
private static final String MORE_NAME = "更多功能按钮,已折叠";
private static final String PARENT_VIDEO_TEXT = "视频通话"; private static final String PARENT_VIDEO_TEXT = "视频通话";
private static final String 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_DELAY_CLICK = 1002;
private static final int MSG_RETRY_STEP = 1003; 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 Handler mHandler;
private Step mCurrentStep = Step.WAITING; private Step mCurrentStep = Step.WAITING;
private String mName = ""; private String mName = "";
@@ -79,15 +92,9 @@ public class DialerAccessibilityService extends AccessibilityService {
private ContactInfo mContactInfo; private ContactInfo mContactInfo;
private int mCallType = ACTION_VIDEO; private int mCallType = ACTION_VIDEO;
private int mFindCount = 0; private int mFindCount = 0;
private static final int MAX_FIND_COUNT = 5;
private final Random mRandom = new Random(); private final Random mRandom = new Random();
private SettingReceiver mSettingReceiver;
public interface AccessibilityEventCallback {
void onAccessibilityEventCallback(AccessibilityEvent accessibilityEvent);
}
private AccessibilityEventCallback mAccessibilityEventCallback;
@Override @Override
public void onCreate() { public void onCreate() {
@@ -97,7 +104,6 @@ public class DialerAccessibilityService extends AccessibilityService {
registerSettingReceiver(); registerSettingReceiver();
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false); mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false);
mAutoHandsFree = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_HNADS_FREE, false); mAutoHandsFree = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_HNADS_FREE, false);
analysisAccessibilityEvent();
} }
private void initHandler() { private void initHandler() {
@@ -110,7 +116,7 @@ public class DialerAccessibilityService extends AccessibilityService {
break; break;
case MSG_DELAY_CLICK: case MSG_DELAY_CLICK:
ClickInfo clickInfo = (ClickInfo) msg.obj; ClickInfo clickInfo = (ClickInfo) msg.obj;
performClick(clickInfo.node, clickInfo.simulate); performClick(clickInfo.node);
if (clickInfo.nextStep != null) { if (clickInfo.nextStep != null) {
mCurrentStep = clickInfo.nextStep; mCurrentStep = clickInfo.nextStep;
sendProcessStepMessage(DEFAULT_DELAY); sendProcessStepMessage(DEFAULT_DELAY);
@@ -122,8 +128,7 @@ public class DialerAccessibilityService extends AccessibilityService {
processCurrentStep(); processCurrentStep();
} else { } else {
Logger.e(TAG, "Max retry count reached, reset to waiting"); Logger.e(TAG, "Max retry count reached, reset to waiting");
mFindCount = 0; resetToWaiting();
mCurrentStep = Step.WAITING;
} }
break; break;
} }
@@ -131,14 +136,19 @@ public class DialerAccessibilityService extends AccessibilityService {
}; };
} }
private void resetToWaiting() {
mFindCount = 0;
mCurrentStep = Step.WAITING;
}
private void sendProcessStepMessage(long delay) { private void sendProcessStepMessage(long delay) {
mHandler.removeMessages(MSG_PROCESS_STEP); mHandler.removeMessages(MSG_PROCESS_STEP);
mHandler.sendEmptyMessageDelayed(MSG_PROCESS_STEP, delay); 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); 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); long delay = DEFAULT_DELAY + mRandom.nextInt(RANDOM_DELAY_RANGE);
mHandler.sendMessageDelayed(msg, delay); mHandler.sendMessageDelayed(msg, delay);
} }
@@ -148,9 +158,6 @@ public class DialerAccessibilityService extends AccessibilityService {
mHandler.sendEmptyMessageDelayed(MSG_RETRY_STEP, delay); mHandler.sendEmptyMessageDelayed(MSG_RETRY_STEP, delay);
} }
private void analysisAccessibilityEvent() {
}
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
Logger.e(TAG, "onStartCommand: "); Logger.e(TAG, "onStartCommand: ");
@@ -184,9 +191,6 @@ public class DialerAccessibilityService extends AccessibilityService {
public void onAccessibilityEvent(AccessibilityEvent event) { public void onAccessibilityEvent(AccessibilityEvent event) {
Logger.v(TAG, "onAccessibilityEvent: event = " + event.toString()); Logger.v(TAG, "onAccessibilityEvent: event = " + event.toString());
checkClassName(event); checkClassName(event);
if (mAccessibilityEventCallback != null) {
mAccessibilityEventCallback.onAccessibilityEventCallback(event);
}
if (mCurrentStep != Step.WAITING) { if (mCurrentStep != Step.WAITING) {
sendProcessStepMessage(100); 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) { private void checkClassName(AccessibilityEvent event) {
Logger.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep); Logger.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep);
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
String currentPackageName = event.getPackageName() != null ? event.getPackageName().toString() : ""; || mCurrentStep == Step.WAITING) {
String currentClassName = event.getClassName() != null ? event.getClassName().toString() : ""; return;
}
switch (mCurrentStep) { String currentClassName = event.getClassName() != null ? event.getClassName().toString() : "";
case WAITING: if ("com.tencent.mm.plugin.account.ui.WelcomeActivity".equals(currentClassName)
if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) { || "com.tencent.mm.plugin.account.ui.LoginPasswordUI".equals(currentClassName)) {
Logger.e(TAG, "checkClassName: to dialer hands free"); PopTip.show("请先登录微信").iconWarning();
} mCurrentStep = Step.WAITING;
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:
}
}
}
} }
} }
@@ -266,7 +269,7 @@ public class DialerAccessibilityService extends AccessibilityService {
putString(mName, Step.CLICK_SEARCH_CONTACT); putString(mName, Step.CLICK_SEARCH_CONTACT);
break; break;
case CLICK_SEARCH_CONTACT: 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); findSearchContactAndClick("com.tencent.mm:id/odf", Step.CLICK_QUICK_WECHAT_CALL);
} else { } else {
PopTip.show("没有找到联系人").iconError(); PopTip.show("没有找到联系人").iconError();
@@ -274,7 +277,7 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
break; break;
case CLICK_QUICK_WECHAT_CALL: case CLICK_QUICK_WECHAT_CALL:
clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET); clickViewById(MORE_FUNCTION_BUTTON_ID, Step.CLICK_TARGET);
break; break;
case CLICK_TARGET: case CLICK_TARGET:
stepCall(Property.TEXT, PARENT_VIDEO_TEXT); stepCall(Property.TEXT, PARENT_VIDEO_TEXT);
@@ -292,7 +295,7 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
break; break;
case FIND_CONTACT: case FIND_CONTACT:
findContactWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); findWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL, false);
break; break;
case FIND_TAG: case FIND_TAG:
findAndClick(Property.TEXT, TAG_TEXT, false, Step.CLICK_TAG); findAndClick(Property.TEXT, TAG_TEXT, false, Step.CLICK_TAG);
@@ -304,10 +307,10 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
break; break;
case CLICK_NAME: case CLICK_NAME:
findContactWithScroll(Property.TEXT, mName, Step.CLICK_INFO); findWithScroll(Property.TEXT, mName, Step.CLICK_INFO, false);
break; break;
case CLICK_INFO: case CLICK_INFO:
findAndClickWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL); findWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL, true);
break; break;
default: default:
} }
@@ -316,43 +319,37 @@ public class DialerAccessibilityService extends AccessibilityService {
private boolean findAndClick(Property type, String text, boolean searchWindows, Step nextStep) { private boolean findAndClick(Property type, String text, boolean searchWindows, Step nextStep) {
AccessibilityNodeInfo node = findNodeByProperty(type, text, searchWindows); AccessibilityNodeInfo node = findNodeByProperty(type, text, searchWindows);
if (node != null && isNodeVisible(node)) { if (node != null && isNodeVisible(node)) {
sendDelayClickMessage(node, false, nextStep); sendDelayClickMessage(node, nextStep);
return true; return true;
} }
return false; 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); AccessibilityNodeInfo node = findNodeByProperty(type, text, false);
if (node != null) { if (node != null) {
if (node.isVisibleToUser()) { if (!checkVisible || node.isVisibleToUser()) {
sendDelayClickMessage(node, false, nextStep); sendDelayClickMessage(node, nextStep);
mFindCount = 0; mFindCount = 0;
} else { } else {
scrollDown(); scrollDown();
sendRetryStepMessage(WAIT_TIME); sendRetryStepMessage(WAIT_TIME);
} }
} else { } else {
handleNotFoundWithRetry("没有找到联系人", nextStep); handleNotFoundWithRetry("没有找到联系人");
} }
} }
private void findContactWithScroll(Property type, String text, Step nextStep) { private void handleNotFoundWithRetry(String errorMsg) {
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) {
if (mFindCount >= MAX_FIND_COUNT) { if (mFindCount >= MAX_FIND_COUNT) {
Logger.e(TAG, "handleNotFoundWithRetry: max count reached"); Logger.e(TAG, "handleNotFoundWithRetry: max count reached");
ToastUtils.showShort(errorMsg); ToastUtils.showShort(errorMsg);
mCurrentStep = Step.WAITING; resetToWaiting();
mFindCount = 0;
} else { } else {
Logger.e(TAG, "handleNotFoundWithRetry: not found, count=" + mFindCount); Logger.e(TAG, "handleNotFoundWithRetry: not found, count=" + mFindCount);
mFindCount++; mFindCount++;
@@ -364,26 +361,19 @@ public class DialerAccessibilityService extends AccessibilityService {
private void handleHandsFree(Property type, String text, boolean isWechat) { private void handleHandsFree(Property type, String text, boolean isWechat) {
AccessibilityNodeInfo node = findNodeByProperty(type, text, true); AccessibilityNodeInfo node = findNodeByProperty(type, text, true);
if (node != null) { if (node != null) {
Point point = getPointByNode(node);
Logger.e(TAG, "handleHandsFree: " + point);
if (isWechat) { if (isWechat) {
clickByPoint(point.x, point.y - 50); doubleClickNodePoint(node);
clickByPoint(point.x, point.y);
} else { } else {
sendDelayClickMessage(node, false, Step.WAITING); sendDelayClickMessage(node, Step.WAITING);
} }
mCurrentStep = Step.WAITING;
} else { } else {
Logger.e(TAG, "handleHandsFree: not found"); Logger.e(TAG, "handleHandsFree: not found");
mCurrentStep = Step.WAITING;
} }
mCurrentStep = Step.WAITING;
} }
private void autoAccept() { private void autoAccept() {
if (findAndClickAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) { if (findAndClickAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION) || clickNode("com.tencent.mm:id/kfp")) {
mCurrentStep = Step.WECHAT_HANDS_FREE;
ToastUtils.showShort("已自动接听视频/语音");
} else if (clickNode("com.tencent.mm:id/kfp", false)) {
mCurrentStep = Step.WECHAT_HANDS_FREE; mCurrentStep = Step.WECHAT_HANDS_FREE;
ToastUtils.showShort("已自动接听视频/语音"); ToastUtils.showShort("已自动接听视频/语音");
} else { } else {
@@ -394,48 +384,43 @@ public class DialerAccessibilityService extends AccessibilityService {
private boolean findAndClickAnswer(Property type, String text) { private boolean findAndClickAnswer(Property type, String text) {
AccessibilityNodeInfo node = findNodeByProperty(type, text, true); AccessibilityNodeInfo node = findNodeByProperty(type, text, true);
if (node != null) { if (node != null) {
Point point = getPointByNode(node); doubleClickNodePoint(node);
Logger.e(TAG, "findAndClickAnswer: " + point);
clickByPoint(point.x, point.y - 50);
clickByPoint(point.x, point.y);
mCurrentStep = Step.WAITING; mCurrentStep = Step.WAITING;
return true; return true;
} }
return false; return false;
} }
private boolean clickNode(String id, boolean simulate) { /**
findFloatWindowNode(id); * 在节点中心点上方与中心点各点击一次(用于接听/免提按钮)
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id); */
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); private void doubleClickNodePoint(AccessibilityNodeInfo node) {
if (optional.isPresent()) { Point point = getPointByNode(node);
AccessibilityNodeInfo node = optional.get(); Logger.e(TAG, "doubleClickNodePoint: " + point);
if (node.isClickable()) { clickByPoint(point.x, point.y - 50);
boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); clickByPoint(point.x, point.y);
Logger.e(TAG, "clickNode: performAction = " + performAction); }
node.recycle();
return performAction; private boolean clickNode(String id) {
} else { AccessibilityNodeInfo node = findFirstNodeById(id);
if (simulate) { if (node == null) {
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 {
Logger.e(TAG, "clickNode: not found"); Logger.e(TAG, "clickNode: not found");
return false; 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) { if (node == null) {
Logger.e(TAG, "performClick: node is null"); Logger.e(TAG, "performClick: node is null");
return; return;
@@ -446,26 +431,20 @@ public class DialerAccessibilityService extends AccessibilityService {
} catch (Exception e) { } catch (Exception e) {
Logger.e(TAG, "performClick: e = " + e.getMessage()); Logger.e(TAG, "performClick: e = " + e.getMessage());
} }
if (node.isClickable()) { boolean clickable = node.isClickable();
boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); boolean performed = false;
Logger.e(TAG, "performClick: performAction = " + performAction); if (clickable) {
if (!performAction) { performed = node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
Rect rect = new Rect(); Logger.e(TAG, "performClick: performAction = " + performed);
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));
} }
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) { private AccessibilityNodeInfo findClickableNode(AccessibilityNodeInfo node) {
@@ -475,52 +454,24 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
if (node.isClickable()) { if (node.isClickable()) {
return node; return node;
} else {
AccessibilityNodeInfo parent = node.getParent();
node.recycle();
return parent != null ? findClickableNode(parent) : null;
}
}
public void findFloatWindowNode(String id) {
List<AccessibilityWindowInfo> 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) { private AccessibilityNodeInfo findNodeByProperty(Property type, String text, boolean searchWindows) {
if (searchWindows) { if (searchWindows) {
return findNodeInWindows(type, text); return findNodeInWindows(type, text);
} else { } else {
return findNodeInActiveWindow(getRootInActiveWindow(), type, text); return findNodeInTree(getRootInActiveWindow(), type, text);
} }
} }
private AccessibilityNodeInfo findNodeInWindows(Property type, String text) { private AccessibilityNodeInfo findNodeInWindows(Property type, String text) {
for (AccessibilityWindowInfo window : getWindows()) { for (AccessibilityWindowInfo window : getWindows()) {
AccessibilityNodeInfo root = window.getRoot(); AccessibilityNodeInfo root = window.getRoot();
AccessibilityNodeInfo node = findNodeInActiveWindow(root, type, text); AccessibilityNodeInfo node = findNodeInTree(root, type, text);
if (node != null) { if (node != null) {
return node; return node;
} }
@@ -532,20 +483,15 @@ public class DialerAccessibilityService extends AccessibilityService {
return null; 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; if (root == null) return null;
Logger.v(TAG, "findNodeInActiveWindow: getText = " + root.getText()); if (checkPropertyMatch(root, type, text)) {
Logger.v(TAG, "findNodeInActiveWindow: getClassName = " + root.getClassName());
Logger.v(TAG, "findNodeInActiveWindow: getContentDescription = " + root.getContentDescription());
boolean satisfied = checkPropertyMatch(root, type, text);
if (satisfied) {
return root; return root;
} else { }
for (int i = 0; i < root.getChildCount(); i++) { for (int i = 0; i < root.getChildCount(); i++) {
AccessibilityNodeInfo result = findNodeInActiveWindow(root.getChild(i), type, text); AccessibilityNodeInfo result = findNodeInTree(root.getChild(i), type, text);
if (result != null) { if (result != null) {
return result; return result;
}
} }
} }
root.recycle(); root.recycle();
@@ -573,37 +519,38 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
private void clickViewById(String id, Step nextStep) { private void clickViewById(String id, Step nextStep) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id); AccessibilityNodeInfo node = findFirstNodeById(id);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); if (node != null) {
if (optional.isPresent()) { sendDelayClickMessage(node, nextStep);
sendDelayClickMessage(optional.get(), false, nextStep);
} else { } else {
findAndClick(Property.DESCRIPTION, SEARCH_TEXT, false, Step.CLICK_SEARCH); findAndClick(Property.DESCRIPTION, SEARCH_TEXT, false, Step.CLICK_SEARCH);
} }
} }
private AccessibilityNodeInfo findFirstNodeById(String id) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
return nodeInfos.isEmpty() ? null : nodeInfos.get(0);
}
private List<AccessibilityNodeInfo> findNodesByViewId(String id) { private List<AccessibilityNodeInfo> findNodesByViewId(String id) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) { if (nodeInfo != null) {
return nodeInfo.findAccessibilityNodeInfosByViewId(id); return nodeInfo.findAccessibilityNodeInfosByViewId(id);
} else {
return new ArrayList<>();
} }
return new ArrayList<>();
} }
private void putString(String text, Step nextStep) { private void putString(String text, Step nextStep) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/d98"); AccessibilityNodeInfo nodeInfo = findFirstNodeById("com.tencent.mm:id/d98");
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); if (nodeInfo != null) {
if (optional.isPresent()) {
AccessibilityNodeInfo nodeInfo = optional.get();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
if (Build.VERSION.SDK_INT >= ACTION_IME_ENTER_VERSION) { if (Build.VERSION.SDK_INT >= ACTION_IME_ENTER_VERSION) {
nodeInfo.performAction(ACTION_IME_ENTER_ID); nodeInfo.performAction(ACTION_IME_ENTER_ID);
} }
hideInputMethod();
mCurrentStep = nextStep; mCurrentStep = nextStep;
sendProcessStepMessage(WAIT_TIME); sendProcessStepMessage(WAIT_TIME);
} else { } else {
@@ -613,31 +560,26 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
private void findSearchContactAndClick(String id, Step nextStep) { private void findSearchContactAndClick(String id, Step nextStep) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id); AccessibilityNodeInfo node = findFirstNodeById(id);
Logger.e(TAG, "findSearchContactAndClick: " + nodeInfos); if (node != null) {
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); sendDelayClickMessage(node, nextStep);
if (optional.isPresent()) {
sendDelayClickMessage(optional.get(), false, nextStep);
} else { } else {
PopTip.show("没有找到联系人").iconError(); PopTip.show("没有找到联系人").iconError();
mCurrentStep = Step.WAITING; mCurrentStep = Step.WAITING;
} }
} }
private boolean stepCall(Property type, String text) { private void stepCall(Property type, String text) {
AccessibilityNodeInfo node = findNodeByProperty(type, text, false); AccessibilityNodeInfo node = findNodeByProperty(type, text, false);
if (node != null) { if (node != null) {
Point point = getPointByNode(node); Point point = getPointByNode(node);
Logger.e(TAG, "stepCall: " + point); Logger.e(TAG, "stepCall: " + point);
clickByPoint(point.x, point.y); clickByPoint(point.x, point.y);
Logger.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done");
mCurrentStep = Step.CLICK_CALL; mCurrentStep = Step.CLICK_CALL;
Logger.e(TAG, "stepCall: next " + mCurrentStep); Logger.e(TAG, "stepCall: next " + mCurrentStep);
sendProcessStepMessage(WAIT_TIME); sendProcessStepMessage(WAIT_TIME);
return true;
} else { } else {
Logger.e(TAG, "stepCall: not found"); Logger.e(TAG, "stepCall: not found");
return false;
} }
} }
@@ -662,62 +604,43 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
private boolean clickByPoint(int x, int y) { private boolean clickByPoint(int x, int y) {
Logger.e(TAG, "clickByPoint: x = " + x); Logger.e(TAG, "clickByPoint: x = " + x + ", y = " + y);
Logger.e(TAG, "clickByPoint: y = " + y);
Point point = new Point(x, y);
Path path = new Path(); Path path = new Path();
path.moveTo(point.x, point.y); path.moveTo(x, y);
GestureDescription.Builder builder = new GestureDescription.Builder();
int duration = 200 + mRandom.nextInt(100); int duration = 200 + mRandom.nextInt(100);
builder.addStroke(new GestureDescription.StrokeDescription(path, 0, duration)); return dispatchGesturePath(path, duration, "clickByPoint");
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;
} }
private boolean scrollScreen(double startY, double endY) { private boolean scrollScreen(double startY, double endY) {
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics(); DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getRealMetrics(dm); wm.getDefaultDisplay().getRealMetrics(dm);
int width = dm.widthPixels; int centerX = dm.widthPixels / 2;
int height = dm.heightPixels; int centerY = dm.heightPixels / 2;
Logger.e(TAG, "scrollScreen: screenWidth = " + width); Logger.e(TAG, "scrollScreen: center position: (" + centerX + "," + centerY + ")");
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 + ")");
Path path = new Path(); Path path = new Path();
path.moveTo(center_X, (int) (center_Y * startY)); path.moveTo(centerX, (int) (centerY * startY));
path.lineTo(center_X, (int) (center_Y * endY)); path.lineTo(centerX, (int) (centerY * endY));
GestureDescription.Builder builder = new GestureDescription.Builder(); return dispatchGesturePath(path, 200, "scrollScreen");
GestureDescription gestureDescription = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build(); }
boolean dispatched = dispatchGesture(gestureDescription, new GestureResultCallback() {
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 @Override
public void onCompleted(GestureDescription gestureDescription) { public void onCompleted(GestureDescription gestureDescription) {
super.onCompleted(gestureDescription); super.onCompleted(gestureDescription);
Logger.d("scrollScreen", "dispatchGesture ScrollUp onCompleted."); Logger.e(tag, "onCompleted: ");
path.close();
} }
@Override @Override
public void onCancelled(GestureDescription gestureDescription) { public void onCancelled(GestureDescription gestureDescription) {
super.onCancelled(gestureDescription); super.onCancelled(gestureDescription);
Logger.d("scrollScreen", "dispatchGesture ScrollUp cancel."); Logger.e(tag, "onCancelled: ");
} }
}, null); }, null);
return dispatched;
} }
private boolean scrollDown() { private boolean scrollDown() {
@@ -728,6 +651,87 @@ public class DialerAccessibilityService extends AccessibilityService {
return scrollScreen(0.5, 1.5); 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.0API 21及以上可通过无障碍窗口类型精确判断
* 低版本无法可靠判断,默认返回 true 以尝试隐藏。
*/
private boolean isInputMethodShown() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
List<AccessibilityWindowInfo> 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 { private enum Step {
WAITING, WAITING,
WECHAT_HANDS_FREE, WECHAT_HANDS_FREE,
@@ -743,8 +747,7 @@ public class DialerAccessibilityService extends AccessibilityService {
FIND_TAG, FIND_TAG,
CLICK_TAG, CLICK_TAG,
CLICK_NAME, CLICK_NAME,
CLICK_INFO, CLICK_INFO
CLICK_VIDEO_CALL
} }
private enum Property { private enum Property {
@@ -754,22 +757,15 @@ public class DialerAccessibilityService extends AccessibilityService {
} }
private static class ClickInfo { private static class ClickInfo {
AccessibilityNodeInfo node; final AccessibilityNodeInfo node;
boolean simulate; final Step nextStep;
Step nextStep;
ClickInfo(AccessibilityNodeInfo node, boolean simulate, Step nextStep) { ClickInfo(AccessibilityNodeInfo node, Step nextStep) {
this.node = node; this.node = node;
this.simulate = simulate;
this.nextStep = nextStep; 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() { private void registerSettingReceiver() {
if (mSettingReceiver == null) { if (mSettingReceiver == null) {
mSettingReceiver = new SettingReceiver(); mSettingReceiver = new SettingReceiver();
@@ -777,7 +773,12 @@ public class DialerAccessibilityService extends AccessibilityService {
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(SETTING_CALL_TYPE_ACTION); filter.addAction(SETTING_CALL_TYPE_ACTION);
filter.addAction(SETTING_AUTOMATIC_ANSWER_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 { private class SettingReceiver extends BroadcastReceiver {
@@ -788,33 +789,19 @@ public class DialerAccessibilityService extends AccessibilityService {
if (TextUtils.isEmpty(action)) return; if (TextUtils.isEmpty(action)) return;
switch (action) { switch (action) {
case SETTING_CALL_TYPE_ACTION: case SETTING_CALL_TYPE_ACTION:
int callType = intent.getIntExtra("call_type", ACTION_VIDEO); mCallType = intent.getIntExtra("call_type", ACTION_VIDEO);
mCallType = callType; Logger.e("SettingReceiver", "onReceive: callType = " + mCallType);
Logger.e("SettingReceiver", "onReceive: callType = " + callType);
break; break;
case SETTING_AUTOMATIC_ANSWER_ACTION: case SETTING_AUTOMATIC_ANSWER_ACTION:
boolean autoAnswer = intent.getBooleanExtra("auto_answer", false); mAutoAccept = intent.getBooleanExtra("auto_answer", false);
mAutoAccept = autoAnswer; Logger.e("SettingReceiver", "onReceive: autoAnswer = " + mAutoAccept);
Logger.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer); break;
case HIDE_IME_ACTION:
hideInputMethod();
break; break;
default: 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());
}
}
} }

View File

@@ -60,6 +60,14 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
private int mCurrentIndex = -1; private int mCurrentIndex = -1;
private int mFragmentSize = 0; private int mFragmentSize = 0;
// 标记 MainActivity 是否已真正退到后台(经历过 onStop
// 用于区分两种场景:
// - 从其他应用按 home 返回(经历过 onStop第一次→ 保持当前 fragment 不重置
// - 已经在主界面时再次按 home未经历 onStop第二次→ 回到默认 fragment
// 注意:不能用 onPause/onResume 判断,因为跳转其他应用时 onPause 会触发而 onResume 不一定,
// 且已在主界面按 home 时可能先 onPause 再 onNewIntent会误判。
private boolean mIsStopped = false;
private ScaleCircleNavigator mScaleCircleNavigator; private ScaleCircleNavigator mScaleCircleNavigator;
private List<AppInfo> mAppInfos; private List<AppInfo> mAppInfos;
@@ -261,14 +269,18 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
super.onNewIntent(intent); super.onNewIntent(intent);
Log.e(TAG, "onNewIntent: " + intent.getAction()); Log.e(TAG, "onNewIntent: " + intent.getAction());
if (Intent.ACTION_MAIN.equals(intent.getAction())) { if (Intent.ACTION_MAIN.equals(intent.getAction())) {
mViewDataBinding.viewPager.setCurrentItem(mDefaultIndex, true); Logger.e(TAG, "onNewIntent: mIsStopped = " + mIsStopped);
if (!mIsStopped) {
// 仍在主界面(未经历 onStop再次按 home 键 → 回到默认 fragment类似 launcher
mViewDataBinding.viewPager.setCurrentItem(mDefaultIndex, true);
}
// 否则:从其他应用按 home 返回(第一次,经历过 onStop→ 保持当前所在的 fragment 不重置
} }
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
//修补autozie fragment item大小不一致
Logger.e(TAG, "onResume: "); Logger.e(TAG, "onResume: ");
mViewModel.getOutsideApp(); mViewModel.getOutsideApp();
mViewModel.getHotseatApp(); mViewModel.getHotseatApp();
@@ -278,6 +290,19 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
} }
} }
@Override
protected void onStart() {
super.onStart();
mIsStopped = false;
}
@Override
protected void onStop() {
super.onStop();
Logger.e(TAG, "onStop: ");
mIsStopped = true;
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();

View File

@@ -1,12 +1,18 @@
package com.ttstd.dialer.activity.settings.call; package com.ttstd.dialer.activity.settings.call;
import android.provider.Settings;
import android.view.View; import android.view.View;
import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.R; import com.ttstd.dialer.R;
import com.ttstd.dialer.activity.settings.home.SettingsViewModel; import com.ttstd.dialer.accessibility.AccessibilityServiceHelper;
import com.ttstd.dialer.accessibility.DialerAccessibilityService;
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
import com.ttstd.dialer.databinding.ActivitySettingsCallBinding; import com.ttstd.dialer.databinding.ActivitySettingsCallBinding;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.view.SwitchButton;
import org.jetbrains.annotations.Nullable;
public class SettingsCallActivity extends BaseMvvmActivity<SettingsCallViewModel, ActivitySettingsCallBinding> { public class SettingsCallActivity extends BaseMvvmActivity<SettingsCallViewModel, ActivitySettingsCallBinding> {
private static final String TAG = "SettingsActivity"; private static final String TAG = "SettingsActivity";
@@ -33,7 +39,24 @@ public class SettingsCallActivity extends BaseMvvmActivity<SettingsCallViewModel
@Override @Override
protected void initView() { protected void initView() {
if (AccessibilityServiceHelper.isAccessibilityServiceEnabled(SettingsCallActivity.this, DialerAccessibilityService.class)) {
mViewDataBinding.siAccessibilityService.setToggleStatu(true);
} else {
mViewDataBinding.siAccessibilityService.setToggleStatu(false);
}
mViewDataBinding.siAccessibilityService.setOnToggleChanged(new SwitchButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(@Nullable SwitchButton view, boolean isChecked) {
if (BuildConfig.DEBUG) {
AccessibilityServiceHelper.setAccessibilityServiceStatus(SettingsCallActivity.this, DialerAccessibilityService.class, isChecked);
String enabledAccessibilityServices = Settings.Secure.getString(getContentResolver(), "enabled_accessibility_services");
Logger.e(TAG, "setAccessibilityServiceStatus: enabledAccessibilityServices = " + enabledAccessibilityServices);
} else {
AccessibilityServiceHelper.jumpToAccessibilitySettings(SettingsCallActivity.this);
}
}
});
} }
@Override @Override

View File

@@ -226,6 +226,10 @@ public class BaseApplication extends Application {
filter.addAction(SystemIntentAction.ACTION_ROLLBACK_COMMITTED); filter.addAction(SystemIntentAction.ACTION_ROLLBACK_COMMITTED);
filter.addDataScheme("package"); filter.addDataScheme("package");
registerReceiver(mAppChangedReceiver, filter); registerReceiver(mAppChangedReceiver, filter);
// 监听系统语言切换,刷新桌面应用名称(该广播不带 package data需单独注册
IntentFilter localeFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
registerReceiver(mAppChangedReceiver, localeFilter);
} }
} }

View File

@@ -40,9 +40,6 @@ public abstract class BaseTransparentActivity extends BaseRxActivity {
.apply(); .apply();
} }
/**
* 修补autozie RecyclerView item大小不一致
*/
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();

View File

@@ -111,6 +111,12 @@ public class CallFragment extends BaseMvvmDialogFragment<CallViewModel, DialogFr
} }
@Override
public void onPause() {
super.onPause();
dismiss();
}
public void call() { public void call() {
try { try {
String phone = mContactInfo.getPhoneNumber(); String phone = mContactInfo.getPhoneNumber();

View File

@@ -495,6 +495,48 @@ public class AppManager {
// 重构getAllApp方法复用现有处理逻辑 // 重构getAllApp方法复用现有处理逻辑
/**
* 系统语言切换时刷新所有已存在应用的名称label
* 因为 label 在首次创建时写入数据库后不会自动更新,
* 切换语言后需要重新读取当前系统语言下的应用名称并同步数据库。
*/
public void refreshAppLabels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
V26Helper.refreshAppLabels(this);
} else {
ASYNC_EXECUTOR.execute(this::refreshAppLabelsSync);
}
}
private void refreshAppLabelsSync() {
try {
List<AppInfo> 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() { public void refreshAllApps() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
V26Helper.refreshAllApps(this); V26Helper.refreshAllApps(this);
@@ -917,6 +959,16 @@ public class AppManager {
}, ASYNC_EXECUTOR); }, ASYNC_EXECUTOR);
} }
@SuppressLint("NewApi")
static void refreshAppLabels(final AppManager manager) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
manager.refreshAppLabelsSync();
}
}, ASYNC_EXECUTOR);
}
@SuppressLint("NewApi") @SuppressLint("NewApi")
static void refreshAllApps(final AppManager manager) { static void refreshAllApps(final AppManager manager) {
CompletableFuture.runAsync(new Runnable() { CompletableFuture.runAsync(new Runnable() {

View File

@@ -29,6 +29,17 @@ public class AppChangedReceiver extends BroadcastReceiver {
if (TextUtils.isEmpty(action)) { if (TextUtils.isEmpty(action)) {
return; 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:", ""); String packageName = intent.getDataString().replace("package:", "");
switch (action) { switch (action) {
case Intent.ACTION_PACKAGE_ADDED: case Intent.ACTION_PACKAGE_ADDED:

View File

@@ -82,6 +82,14 @@
android:background="@drawable/settings_card_bg" android:background="@drawable/settings_card_bg"
android:orientation="vertical"> android:orientation="vertical">
<com.ttstd.dialer.view.SettingItem
android:id="@+id/si_accessibility_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:disableText="未开启,无障碍服务不可用"
app:enableText="已开启"
app:optionsText="无障碍服务" />
<com.ttstd.dialer.view.SettingItem <com.ttstd.dialer.view.SettingItem
android:id="@+id/si_fast_call_phone" android:id="@+id/si_fast_call_phone"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -17,8 +17,8 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="@dimen/dp_16"
android:layout_marginEnd="8dp" android:layout_marginEnd="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_button" app:layout_constraintEnd_toStartOf="@+id/switch_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -53,7 +53,7 @@
android:id="@+id/switch_button" android:id="@+id/switch_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginEnd="@dimen/dp_16"
app:sb_show_indicator="false" app:sb_show_indicator="false"
app:sb_checked="false" app:sb_checked="false"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@@ -62,9 +62,9 @@
<ImageView <ImageView
android:id="@+id/iv_more" android:id="@+id/iv_more"
android:layout_width="20dp" android:layout_width="@dimen/dp_20"
android:layout_height="20dp" android:layout_height="@dimen/dp_20"
android:layout_marginEnd="16dp" android:layout_marginEnd="@dimen/dp_16"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/icon_more" android:src="@drawable/icon_more"
@@ -77,8 +77,8 @@
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1px" android:layout_height="1px"
android:layout_marginStart="16dp" android:layout_marginStart="@dimen/dp_16"
android:layout_marginEnd="16dp" android:layout_marginEnd="@dimen/dp_16"
android:background="@color/lightGray" android:background="@color/lightGray"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@@ -11,7 +11,7 @@
<string name="accessibility_service_description">使用拨号助手一键拨号</string> <string name="accessibility_service_description">使用拨号助手一键拨号</string>
<!--call start--> <!--call start-->
<string name="options_text_fast_call_phone">开启快捷通话</string> <string name="options_text_fast_call_phone">快捷通话</string>
<string name="enable_text_fast_call_phone">已开启,快捷联系人通话类型</string> <string name="enable_text_fast_call_phone">已开启,快捷联系人通话类型</string>
<string name="disable_text_fast_call_phone">未开启,快捷联系人通话类型</string> <string name="disable_text_fast_call_phone">未开启,快捷联系人通话类型</string>
@@ -23,7 +23,7 @@
<string name="enable_text_auto_accept">已开启,自动接听视频和语音</string> <string name="enable_text_auto_accept">已开启,自动接听视频和语音</string>
<string name="disable_text_auto_accept">未开启,自动接听视频和语音</string> <string name="disable_text_auto_accept">未开启,自动接听视频和语音</string>
<string name="options_text_hands_free">自动开免提</string> <string name="options_text_hands_free">自动开免提</string>
<string name="enable_text_hands_free">已开启,自动开启免提</string> <string name="enable_text_hands_free">已开启,自动开启免提</string>
<string name="disable_text_hands_free">未开启,自动开启免提</string> <string name="disable_text_hands_free">未开启,自动开启免提</string>
@@ -33,7 +33,7 @@
<!--call end--> <!--call end-->
<!--utils start--> <!--utils start-->
<string name="options_text_float">开启悬浮按钮</string> <string name="options_text_float">全局悬浮按钮</string>
<string name="enable_text_float">已开启,点小圆点可以直接返回桌面</string> <string name="enable_text_float">已开启,点小圆点可以直接返回桌面</string>
<string name="disable_text_float">未开启,点小圆点可以直接返回桌面</string> <string name="disable_text_float">未开启,点小圆点可以直接返回桌面</string>
@@ -45,7 +45,7 @@
<string name="enable_text_default_launcher">已设置为默认桌面</string> <string name="enable_text_default_launcher">已设置为默认桌面</string>
<string name="disable_text_default_launcher">未设置为默认桌面</string> <string name="disable_text_default_launcher">未设置为默认桌面</string>
<string name="options_text_hourly_chime">开启整点报时</string> <string name="options_text_hourly_chime">整点报时</string>
<string name="enable_text_hourly_chime">已开启,整点时将自动语音报时</string> <string name="enable_text_hourly_chime">已开启,整点时将自动语音报时</string>
<string name="disable_text_hourly_chime">未开启,整点时将自动语音报时</string> <string name="disable_text_hourly_chime">未开启,整点时将自动语音报时</string>