821 lines
32 KiB
Java
821 lines
32 KiB
Java
package com.ttstd.dialer.service;
|
|
|
|
import android.accessibilityservice.AccessibilityService;
|
|
import android.accessibilityservice.GestureDescription;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.graphics.Path;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.text.TextUtils;
|
|
import android.util.DisplayMetrics;
|
|
import android.view.WindowManager;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.view.accessibility.AccessibilityWindowInfo;
|
|
|
|
import com.blankj.utilcode.util.ToastUtils;
|
|
import com.hjq.toast.Toaster;
|
|
import com.tencent.mmkv.MMKV;
|
|
import com.ttstd.dialer.config.CommonConfig;
|
|
import com.ttstd.dialer.db.contact.ContactInfo;
|
|
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);
|
|
|
|
public static final int ACTION_VIDEO = 1;
|
|
public static final int ACTION_AUDIO = 2;
|
|
|
|
private static final int ACTION_IME_ENTER_VERSION = 30;
|
|
private static final int ACTION_IME_ENTER_ID = 16908372;
|
|
|
|
private static final String DIALER_TEXT = "音视频通话";
|
|
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 = "视频通话";
|
|
private static final String CALL_TEXT = "语音通话";
|
|
|
|
private static final String RECEIVE_DESCRIPTION = "接听";
|
|
private static final String HANDS_FREE_TEXT = "扬声器已关";
|
|
private static final String DIALER_HANDS_FREE_TEXT = "免提";
|
|
private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭";
|
|
|
|
private static final int WAIT_TIME = 1600;
|
|
private static final int DEFAULT_DELAY = 500;
|
|
private static final int RANDOM_DELAY_RANGE = 300;
|
|
|
|
private static final int MSG_PROCESS_STEP = 1001;
|
|
private static final int MSG_DELAY_CLICK = 1002;
|
|
private static final int MSG_RETRY_STEP = 1003;
|
|
|
|
private Handler mHandler;
|
|
private Step mCurrentStep = Step.WAITING;
|
|
private String mName = "";
|
|
private String mTagName = "";
|
|
|
|
private boolean mAutoAccept = false;
|
|
private boolean mAutoHandsFree = false;
|
|
|
|
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;
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
Logger.e(TAG, "onCreate: ");
|
|
initHandler();
|
|
registerSettingReceiver();
|
|
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false);
|
|
mAutoHandsFree = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_HNADS_FREE, false);
|
|
analysisAccessibilityEvent();
|
|
}
|
|
|
|
private void initHandler() {
|
|
mHandler = new Handler(Looper.getMainLooper()) {
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
switch (msg.what) {
|
|
case MSG_PROCESS_STEP:
|
|
processCurrentStep();
|
|
break;
|
|
case MSG_DELAY_CLICK:
|
|
ClickInfo clickInfo = (ClickInfo) msg.obj;
|
|
performClick(clickInfo.node, clickInfo.simulate);
|
|
if (clickInfo.nextStep != null) {
|
|
mCurrentStep = clickInfo.nextStep;
|
|
sendProcessStepMessage(DEFAULT_DELAY);
|
|
}
|
|
break;
|
|
case MSG_RETRY_STEP:
|
|
mFindCount++;
|
|
if (mFindCount <= MAX_FIND_COUNT) {
|
|
processCurrentStep();
|
|
} else {
|
|
Logger.e(TAG, "Max retry count reached, reset to waiting");
|
|
mFindCount = 0;
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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) {
|
|
Message msg = mHandler.obtainMessage(MSG_DELAY_CLICK);
|
|
msg.obj = new ClickInfo(node, simulate, nextStep);
|
|
long delay = DEFAULT_DELAY + mRandom.nextInt(RANDOM_DELAY_RANGE);
|
|
mHandler.sendMessageDelayed(msg, delay);
|
|
}
|
|
|
|
private void sendRetryStepMessage(long delay) {
|
|
mHandler.removeMessages(MSG_RETRY_STEP);
|
|
mHandler.sendEmptyMessageDelayed(MSG_RETRY_STEP, delay);
|
|
}
|
|
|
|
private void analysisAccessibilityEvent() {
|
|
}
|
|
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
Logger.e(TAG, "onStartCommand: ");
|
|
if (intent != null) {
|
|
mContactInfo = (ContactInfo) intent.getSerializableExtra("ContactInfo");
|
|
Logger.e(TAG, "onStartCommand: contactInfo = " + mContactInfo);
|
|
mCallType = intent.getIntExtra("call_type", ACTION_VIDEO);
|
|
mName = mContactInfo != null ? mContactInfo.getName() : "";
|
|
mCurrentStep = Step.CLICK_HOME;
|
|
if (mContactInfo != null) {
|
|
startWeixin();
|
|
sendProcessStepMessage(WAIT_TIME);
|
|
}
|
|
}
|
|
return super.onStartCommand(intent, flags, startId);
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
Logger.e(TAG, "onDestroy: ");
|
|
if (mHandler != null) {
|
|
mHandler.removeCallbacksAndMessages(null);
|
|
}
|
|
if (mSettingReceiver != null) {
|
|
unregisterReceiver(mSettingReceiver);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
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);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onInterrupt() {
|
|
|
|
}
|
|
|
|
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":
|
|
Toaster.showLong("请先登录微信");
|
|
mCurrentStep = Step.WAITING;
|
|
break;
|
|
case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI":
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void processCurrentStep() {
|
|
Logger.e(TAG, "processCurrentStep: " + mCurrentStep);
|
|
switch (mCurrentStep) {
|
|
case WAITING:
|
|
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false);
|
|
Logger.e(TAG, "processCurrentStep: mAutoAccept = " + mAutoAccept);
|
|
if (mAutoAccept) {
|
|
autoAccept();
|
|
}
|
|
break;
|
|
case WECHAT_HANDS_FREE:
|
|
mAutoHandsFree = mMMKV.decodeInt(CommonConfig.WECHAT_AUTO_HNADS_FREE, 0) == 1;
|
|
Logger.e(TAG, "processCurrentStep: mAutoHandsFree = " + mAutoHandsFree);
|
|
if (mAutoHandsFree) {
|
|
handleHandsFree(Property.DESCRIPTION, HANDS_FREE_TEXT, true);
|
|
} else {
|
|
Logger.e(TAG, "processCurrentStep: not enable auto handsfree");
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
break;
|
|
case DIALER_HANDS_FREE:
|
|
if (findNodeByProperty(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT, true) != null) {
|
|
handleHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT, false);
|
|
} else {
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
break;
|
|
case CLICK_HOME:
|
|
if (findAndClick(Property.TEXT, mName, false, Step.CLICK_QUICK_WECHAT_CALL)) {
|
|
Logger.e(TAG, "processCurrentStep: found contact in home");
|
|
} else {
|
|
clickViewById("com.tencent.mm:id/jha", Step.CLICK_SEARCH);
|
|
}
|
|
break;
|
|
case CLICK_SEARCH:
|
|
putString(mName, Step.CLICK_SEARCH_CONTACT);
|
|
break;
|
|
case CLICK_SEARCH_CONTACT:
|
|
if (findNodesByViewId("com.tencent.mm:id/gzf").size() > 0) {
|
|
findSearchContactAndClick("com.tencent.mm:id/odf", Step.CLICK_QUICK_WECHAT_CALL);
|
|
} else {
|
|
Toaster.show("没有找到联系人");
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
break;
|
|
case CLICK_QUICK_WECHAT_CALL:
|
|
clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET);
|
|
break;
|
|
case CLICK_TARGET:
|
|
stepCall(Property.TEXT, PARENT_VIDEO_TEXT);
|
|
break;
|
|
case CLICK_CALL:
|
|
if (mCallType == ACTION_AUDIO) {
|
|
findAndClick(Property.TEXT, VIDEO_TEXT, false, Step.WAITING);
|
|
} else if (mCallType == ACTION_VIDEO) {
|
|
findAndClick(Property.TEXT, CALL_TEXT, false, Step.WAITING);
|
|
}
|
|
break;
|
|
case CLICK_CONTACT:
|
|
if (!findAndClick(Property.TEXT, CONTACT_TEXT, false, Step.FIND_TAG)) {
|
|
touchContact();
|
|
}
|
|
break;
|
|
case FIND_CONTACT:
|
|
findContactWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
|
|
break;
|
|
case FIND_TAG:
|
|
findAndClick(Property.TEXT, TAG_TEXT, false, Step.CLICK_TAG);
|
|
break;
|
|
case CLICK_TAG:
|
|
if (!findAndClick(Property.TEXT, mTagName, false, Step.CLICK_NAME)) {
|
|
Toaster.show("没有找到标签");
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
break;
|
|
case CLICK_NAME:
|
|
findContactWithScroll(Property.TEXT, mName, Step.CLICK_INFO);
|
|
break;
|
|
case CLICK_INFO:
|
|
findAndClickWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL);
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
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);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void findAndClickWithScroll(Property type, String text, Step nextStep) {
|
|
AccessibilityNodeInfo node = findNodeByProperty(type, text, false);
|
|
if (node != null) {
|
|
if (node.isVisibleToUser()) {
|
|
sendDelayClickMessage(node, false, nextStep);
|
|
mFindCount = 0;
|
|
} else {
|
|
scrollDown();
|
|
sendRetryStepMessage(WAIT_TIME);
|
|
}
|
|
} else {
|
|
handleNotFoundWithRetry("没有找到联系人", nextStep);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (mFindCount >= MAX_FIND_COUNT) {
|
|
Logger.e(TAG, "handleNotFoundWithRetry: max count reached");
|
|
ToastUtils.showShort(errorMsg);
|
|
mCurrentStep = Step.WAITING;
|
|
mFindCount = 0;
|
|
} else {
|
|
Logger.e(TAG, "handleNotFoundWithRetry: not found, count=" + mFindCount);
|
|
mFindCount++;
|
|
scrollDown();
|
|
sendRetryStepMessage(WAIT_TIME);
|
|
}
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
sendDelayClickMessage(node, false, Step.WAITING);
|
|
}
|
|
mCurrentStep = Step.WAITING;
|
|
} else {
|
|
Logger.e(TAG, "handleHandsFree: not found");
|
|
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)) {
|
|
mCurrentStep = Step.WECHAT_HANDS_FREE;
|
|
ToastUtils.showShort("已自动接听视频/语音");
|
|
} else {
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
}
|
|
|
|
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);
|
|
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 {
|
|
Logger.e(TAG, "clickNode: not found");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void performClick(AccessibilityNodeInfo node, boolean simulate) {
|
|
if (node == null) {
|
|
Logger.e(TAG, "performClick: node is null");
|
|
return;
|
|
}
|
|
try {
|
|
Logger.e(TAG, "performClick: getText = " + node.getText());
|
|
Logger.e(TAG, "performClick: isClickable = " + node.isClickable());
|
|
} 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));
|
|
}
|
|
}
|
|
|
|
private AccessibilityNodeInfo findClickableNode(AccessibilityNodeInfo node) {
|
|
if (node == null) {
|
|
Logger.e(TAG, "findClickableNode: node is null");
|
|
return null;
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
|
|
private AccessibilityNodeInfo findNodeByProperty(Property type, String text, boolean searchWindows) {
|
|
if (searchWindows) {
|
|
return findNodeInWindows(type, text);
|
|
} else {
|
|
return findNodeInActiveWindow(getRootInActiveWindow(), type, text);
|
|
}
|
|
}
|
|
|
|
private AccessibilityNodeInfo findNodeInWindows(Property type, String text) {
|
|
for (AccessibilityWindowInfo window : getWindows()) {
|
|
AccessibilityNodeInfo root = window.getRoot();
|
|
AccessibilityNodeInfo node = findNodeInActiveWindow(root, type, text);
|
|
if (node != null) {
|
|
return node;
|
|
}
|
|
if (root != null) {
|
|
root.recycle();
|
|
}
|
|
}
|
|
Logger.e(TAG, "findNodeInWindows: not found");
|
|
return null;
|
|
}
|
|
|
|
private AccessibilityNodeInfo findNodeInActiveWindow(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) {
|
|
return root;
|
|
} else {
|
|
for (int i = 0; i < root.getChildCount(); i++) {
|
|
AccessibilityNodeInfo result = findNodeInActiveWindow(root.getChild(i), type, text);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
root.recycle();
|
|
return null;
|
|
}
|
|
|
|
private boolean checkPropertyMatch(AccessibilityNodeInfo node, Property type, String text) {
|
|
switch (type) {
|
|
case TEXT:
|
|
return node.getText() != null && text.contentEquals(node.getText());
|
|
case CLASS_NAME:
|
|
return node.getClassName() != null && text.contentEquals(node.getClassName());
|
|
case DESCRIPTION:
|
|
return node.getContentDescription() != null && text.contentEquals(node.getContentDescription());
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean isNodeVisible(AccessibilityNodeInfo node) {
|
|
if (node == null) return false;
|
|
Rect rect = new Rect();
|
|
node.getBoundsInScreen(rect);
|
|
return rect.left >= 0 && rect.top >= 0 && rect.right >= 0 && rect.bottom >= 0;
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
findAndClick(Property.DESCRIPTION, SEARCH_TEXT, false, Step.CLICK_SEARCH);
|
|
}
|
|
}
|
|
|
|
private List<AccessibilityNodeInfo> findNodesByViewId(String id) {
|
|
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
|
|
if (nodeInfo != null) {
|
|
return nodeInfo.findAccessibilityNodeInfosByViewId(id);
|
|
} else {
|
|
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();
|
|
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);
|
|
}
|
|
mCurrentStep = nextStep;
|
|
sendProcessStepMessage(WAIT_TIME);
|
|
} else {
|
|
Toaster.show("没有找到搜索框");
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
Toaster.show("没有找到联系人");
|
|
mCurrentStep = Step.WAITING;
|
|
}
|
|
}
|
|
|
|
private boolean 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;
|
|
}
|
|
}
|
|
|
|
private void touchContact() {
|
|
boolean successful = clickByPoint(268, 1440);
|
|
if (successful) {
|
|
mCurrentStep = Step.FIND_TAG;
|
|
sendProcessStepMessage(WAIT_TIME);
|
|
} else {
|
|
mCurrentStep = Step.WAITING;
|
|
Toaster.show("点击失败,请重试");
|
|
}
|
|
}
|
|
|
|
static Point getPointByNode(AccessibilityNodeInfo node) {
|
|
if (node == null) {
|
|
return new Point(0, 0);
|
|
}
|
|
Rect rect = new Rect();
|
|
node.getBoundsInScreen(rect);
|
|
return new Point(rect.centerX(), rect.centerY());
|
|
}
|
|
|
|
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);
|
|
Path path = new Path();
|
|
path.moveTo(point.x, point.y);
|
|
GestureDescription.Builder builder = new GestureDescription.Builder();
|
|
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;
|
|
}
|
|
|
|
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 + ")");
|
|
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() {
|
|
@Override
|
|
public void onCompleted(GestureDescription gestureDescription) {
|
|
super.onCompleted(gestureDescription);
|
|
Logger.d("scrollScreen", "dispatchGesture ScrollUp onCompleted.");
|
|
path.close();
|
|
}
|
|
|
|
@Override
|
|
public void onCancelled(GestureDescription gestureDescription) {
|
|
super.onCancelled(gestureDescription);
|
|
Logger.d("scrollScreen", "dispatchGesture ScrollUp cancel.");
|
|
}
|
|
}, null);
|
|
return dispatched;
|
|
}
|
|
|
|
private boolean scrollDown() {
|
|
return scrollScreen(1.5, 0.5);
|
|
}
|
|
|
|
private boolean scrollUp() {
|
|
return scrollScreen(0.5, 1.5);
|
|
}
|
|
|
|
private enum Step {
|
|
WAITING,
|
|
WECHAT_HANDS_FREE,
|
|
DIALER_HANDS_FREE,
|
|
CLICK_HOME,
|
|
CLICK_SEARCH,
|
|
CLICK_SEARCH_CONTACT,
|
|
CLICK_QUICK_WECHAT_CALL,
|
|
CLICK_TARGET,
|
|
CLICK_CALL,
|
|
CLICK_CONTACT,
|
|
FIND_CONTACT,
|
|
FIND_TAG,
|
|
CLICK_TAG,
|
|
CLICK_NAME,
|
|
CLICK_INFO,
|
|
CLICK_VIDEO_CALL
|
|
}
|
|
|
|
private enum Property {
|
|
TEXT,
|
|
CLASS_NAME,
|
|
DESCRIPTION
|
|
}
|
|
|
|
private static class ClickInfo {
|
|
AccessibilityNodeInfo node;
|
|
boolean simulate;
|
|
Step nextStep;
|
|
|
|
ClickInfo(AccessibilityNodeInfo node, boolean simulate, 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();
|
|
}
|
|
IntentFilter filter = new IntentFilter();
|
|
filter.addAction(SETTING_CALL_TYPE_ACTION);
|
|
filter.addAction(SETTING_AUTOMATIC_ANSWER_ACTION);
|
|
registerReceiver(mSettingReceiver, filter);
|
|
}
|
|
|
|
private class SettingReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
Logger.e("SettingReceiver", "onReceive: " + action);
|
|
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);
|
|
break;
|
|
case SETTING_AUTOMATIC_ANSWER_ACTION:
|
|
boolean autoAnswer = intent.getBooleanExtra("auto_answer", false);
|
|
mAutoAccept = autoAnswer;
|
|
Logger.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer);
|
|
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());
|
|
}
|
|
}
|
|
}
|