refactor(accessibility): 重构无障碍服务辅助类并优化微信拨号逻辑
- 新增 AccessibilityServiceHelper 类,提供无障碍服务启用状态检查、列表获取和状态设置功能 - 优化 DialerAccessibilityService 中的点击操作,移除模拟点击参数,统一使用节点点击方式 - 简化微信自动接听逻辑,合并多个相似方法为 findWithScroll 统一处理滚动查找 - 移除不再使用的回调接口和常量,精简代码结构 - 修复 MainActivity 中页面跳转逻辑,正确区分应用前后台切换状态 - 在设置页面集成无障碍服务开关控制,支持调试模式下直接启用服务 - 添加输入法隐藏功能,兼容不同安卓版本的键盘管理机制 - 优化节点查找算法,提升微信联系人搜索和拨号的稳定性
This commit is contained in:
@@ -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<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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 跳转到系统的无障碍服务设置页面
|
||||
*
|
||||
|
||||
@@ -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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
Optional<AccessibilityNodeInfo> 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<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) {
|
||||
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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
Optional<AccessibilityNodeInfo> 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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
return nodeInfos.isEmpty() ? null : nodeInfos.get(0);
|
||||
}
|
||||
|
||||
private List<AccessibilityNodeInfo> 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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/d98");
|
||||
Optional<AccessibilityNodeInfo> 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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
Logger.e(TAG, "findSearchContactAndClick: " + nodeInfos);
|
||||
Optional<AccessibilityNodeInfo> 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<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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,14 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
private int mCurrentIndex = -1;
|
||||
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 List<AppInfo> mAppInfos;
|
||||
@@ -261,14 +269,18 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
super.onNewIntent(intent);
|
||||
Log.e(TAG, "onNewIntent: " + 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
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
//修补autozie fragment item大小不一致
|
||||
Logger.e(TAG, "onResume: ");
|
||||
mViewModel.getOutsideApp();
|
||||
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
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package com.ttstd.dialer.activity.settings.call;
|
||||
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.BuildConfig;
|
||||
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.databinding.ActivitySettingsBinding;
|
||||
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> {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
@@ -33,7 +39,24 @@ public class SettingsCallActivity extends BaseMvvmActivity<SettingsCallViewModel
|
||||
|
||||
@Override
|
||||
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
|
||||
|
||||
@@ -226,6 +226,10 @@ public class BaseApplication extends Application {
|
||||
filter.addAction(SystemIntentAction.ACTION_ROLLBACK_COMMITTED);
|
||||
filter.addDataScheme("package");
|
||||
registerReceiver(mAppChangedReceiver, filter);
|
||||
|
||||
// 监听系统语言切换,刷新桌面应用名称(该广播不带 package data,需单独注册)
|
||||
IntentFilter localeFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
|
||||
registerReceiver(mAppChangedReceiver, localeFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ public abstract class BaseTransparentActivity extends BaseRxActivity {
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修补autozie RecyclerView item大小不一致
|
||||
*/
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
@@ -111,6 +111,12 @@ public class CallFragment extends BaseMvvmDialogFragment<CallViewModel, DialogFr
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
public void call() {
|
||||
try {
|
||||
String phone = mContactInfo.getPhoneNumber();
|
||||
|
||||
@@ -495,6 +495,48 @@ public class AppManager {
|
||||
|
||||
|
||||
// 重构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() {
|
||||
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() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
android:background="@drawable/settings_card_bg"
|
||||
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
|
||||
android:id="@+id/si_fast_call_phone"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="@dimen/dp_16"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/switch_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -53,7 +53,7 @@
|
||||
android:id="@+id/switch_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
app:sb_show_indicator="false"
|
||||
app:sb_checked="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -62,9 +62,9 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_more"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_width="@dimen/dp_20"
|
||||
android:layout_height="@dimen/dp_20"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/icon_more"
|
||||
@@ -77,8 +77,8 @@
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="@dimen/dp_16"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
android:background="@color/lightGray"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="accessibility_service_description">使用拨号助手一键拨号</string>
|
||||
|
||||
<!--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="disable_text_fast_call_phone">未开启,快捷联系人通话类型</string>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="enable_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="disable_text_hands_free">未开启,自动开启免提</string>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<!--call end-->
|
||||
|
||||
<!--utils start-->
|
||||
<string name="options_text_float">开启悬浮按钮</string>
|
||||
<string name="options_text_float">全局悬浮按钮</string>
|
||||
<string name="enable_text_float">已开启,点小圆点可以直接返回桌面</string>
|
||||
<string name="disable_text_float">未开启,点小圆点可以直接返回桌面</string>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<string name="enable_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="disable_text_hourly_chime">未开启,整点时将自动语音报时</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user