通过nfc实时读取数据并拨打电话
This commit is contained in:
@@ -1,28 +1,127 @@
|
||||
package com.google.android.accessibility.selecttospeak;
|
||||
|
||||
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.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityWindowInfo;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import com.ttstd.elderlyassistant.bean.WechatContact;
|
||||
import com.ttstd.elderlyassistant.config.CommonConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.annotations.NonNull;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.ObservableEmitter;
|
||||
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
|
||||
/**
|
||||
* 通过微信标签最高支持8.0.49,8.0.50 获取不到数据
|
||||
* 8.0.54 可以获取
|
||||
* 通过 {@link AccessibilityService#getWindows}和修改accessibility-service 配置能遍历屏幕元素
|
||||
*/
|
||||
public class SelectToSpeakService extends AccessibilityService {
|
||||
private static final String TAG = "SelectToSpeakService";
|
||||
|
||||
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||
|
||||
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 TAG_TEXT = "标签";
|
||||
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 = "免提,已关闭";
|
||||
|
||||
public static final int TYPE_VOICE = 0;
|
||||
public static final int TYPE_VIDEO = 1;
|
||||
|
||||
private static final int WAIT_TIME = 1500;
|
||||
|
||||
private int mCallType = TYPE_VOICE;
|
||||
|
||||
private WechatContact mContact;
|
||||
private Step mCurrentStep = Step.WAITING;
|
||||
private String mName = "";//微信昵称
|
||||
private boolean mAutoAccept = false;
|
||||
|
||||
private AccessibilityEvent input = null;
|
||||
|
||||
public interface AccessibilityEventCallback {
|
||||
public void onAccessibilityEventCallback(AccessibilityEvent accessibilityEvent);
|
||||
}
|
||||
|
||||
private AccessibilityEventCallback mAccessibilityEventCallback;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.e(TAG, "onCreate: ");
|
||||
registerSettingReceiver();
|
||||
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
|
||||
dealingAccessibilityEvent();
|
||||
|
||||
}
|
||||
|
||||
private void dealingAccessibilityEvent() {
|
||||
Observable.create(new ObservableOnSubscribe<AccessibilityEvent>() {
|
||||
@Override
|
||||
public void subscribe(@NonNull ObservableEmitter<AccessibilityEvent> emitter) throws Throwable {
|
||||
mAccessibilityEventCallback = emitter::onNext;
|
||||
}
|
||||
}).throttleLast(WAIT_TIME, TimeUnit.MILLISECONDS)
|
||||
.subscribe(new Consumer<AccessibilityEvent>() {
|
||||
@Override
|
||||
public void accept(AccessibilityEvent accessibilityEvent) throws Throwable {
|
||||
Log.e(TAG, "accept: ");
|
||||
_onAccessibilityEvent(accessibilityEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.e(TAG, "onStartCommand: ");
|
||||
|
||||
if (intent != null) {
|
||||
mContact = (WechatContact) intent.getSerializableExtra("WechatInfo");
|
||||
Log.e(TAG, "onStartCommand: wechatInfo = " + mContact);
|
||||
mCallType = intent.getIntExtra("call_type", TYPE_VOICE);
|
||||
mName = mContact.getName();
|
||||
mCurrentStep = Step.CLICK_HOME;
|
||||
launchWeChat();
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@@ -30,19 +129,159 @@ public class SelectToSpeakService extends AccessibilityService {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.e(TAG, "onDestroy: ");
|
||||
|
||||
if (mSettingBroadcastReceiver != null) {
|
||||
unregisterReceiver(mSettingBroadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
List<AccessibilityWindowInfo> accessibilityWindowInfos = getWindows();
|
||||
Log.e(TAG, "onAccessibilityEvent: getWindows = " + accessibilityWindowInfos);
|
||||
Log.v(TAG, "onAccessibilityEvent: event = " + event.toString());
|
||||
checkClassName(event);
|
||||
mAccessibilityEventCallback.onAccessibilityEventCallback(event);
|
||||
}
|
||||
|
||||
private void checkClassName(AccessibilityEvent event) {
|
||||
Log.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep);
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
String currentPackageName = event.getPackageName().toString();
|
||||
String currentClassName = event.getClassName().toString();
|
||||
|
||||
switch (mCurrentStep) {
|
||||
case WAITING:
|
||||
if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) {
|
||||
Log.e(TAG, "checkClassName: to dialer hands free");
|
||||
// mCurrentStep = Step.DIALER_HANDS_FREE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!TextUtils.isEmpty(currentClassName)) {
|
||||
switch (currentClassName) {
|
||||
case "com.tencent.mm.ui.LauncherUI":
|
||||
// if (mCurrentStep != Step.FIND_CONTACT) {
|
||||
// mCurrentStep = Step.CLICK_CONTACT;
|
||||
// }
|
||||
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:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1.在微信页面直接找到联系人拨打电话
|
||||
* 2.在联系人页面找到并拨打
|
||||
* 3.通过进入联系人-标签找到并拨打
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
private void _onAccessibilityEvent(AccessibilityEvent event) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: " + mCurrentStep);
|
||||
switch (mCurrentStep) {
|
||||
case WAITING:
|
||||
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
|
||||
Log.e(TAG, "_onAccessibilityEvent: mAutoAccept = " + mAutoAccept);
|
||||
if (!mAutoAccept) {
|
||||
return;
|
||||
}
|
||||
if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) {
|
||||
mCurrentStep = Step.WECHAT_HANDS_FREE;
|
||||
Toast.makeText(this, "已自动接听视频/语音", Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
mCurrentStep = Step.WAITING;
|
||||
// clickAnswer();
|
||||
}
|
||||
break;
|
||||
case WECHAT_HANDS_FREE:
|
||||
handsFree(Property.DESCRIPTION, HANDS_FREE_TEXT);
|
||||
break;
|
||||
case DIALER_HANDS_FREE:
|
||||
if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) {
|
||||
dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT);
|
||||
} else {
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
break;
|
||||
case CLICK_HOME://主页能找到直接点击进去更多
|
||||
if (stepHome(Property.TEXT, mName)) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: not found contact in home");
|
||||
} else {
|
||||
clickViewById("com.tencent.mm:id/jha", Step.CLICK_SEARCH);
|
||||
// step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH);
|
||||
}
|
||||
break;
|
||||
case CLICK_SEARCH:
|
||||
putString(mName, Step.CLICK_SEARCH_CONTACT);
|
||||
break;
|
||||
case CLICK_SEARCH_CONTACT:
|
||||
if (findSearchContact(Step.FIND_CONTACT)) {
|
||||
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
|
||||
} else {
|
||||
Toaster.show("没有找到联系人");
|
||||
}
|
||||
break;
|
||||
case CLICK_QUICK_WECHAT_CALL://点击更多页面
|
||||
clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET);
|
||||
// step(Property.DESCRIPTION, MORE_NAME, Step.CLICK_TARGET);
|
||||
break;
|
||||
case CLICK_TARGET://点击视频通话
|
||||
stepCall(Property.TEXT, PARENT_VIDEO_TEXT);
|
||||
// clickVideoCall();
|
||||
break;
|
||||
|
||||
case CLICK_CONTACT://进入通讯录界面
|
||||
if (stepHome(Property.TEXT, CONTACT_TEXT, Step.FIND_TAG)) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: enter contact");
|
||||
} else {
|
||||
touchContact();
|
||||
}
|
||||
break;
|
||||
case FIND_CONTACT://模拟滑动找到联系人
|
||||
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
|
||||
break;
|
||||
case FIND_TAG:
|
||||
step(Property.TEXT, TAG_TEXT, Step.CLICK_TAG);
|
||||
break;
|
||||
case CLICK_TAG:
|
||||
|
||||
break;
|
||||
case CLICK_NAME://点击item
|
||||
findContact(Property.TEXT, mName, Step.CLICK_INFO);
|
||||
break;
|
||||
case CLICK_INFO://进入个人信息页面
|
||||
stepCallDialog(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL);
|
||||
break;
|
||||
case CLICK_CALL://打视频或者电话
|
||||
if (mCallType == TYPE_VIDEO) {
|
||||
step(Property.TEXT, VIDEO_TEXT, Step.WAITING);
|
||||
} else if (mCallType == TYPE_VOICE) {
|
||||
step(Property.TEXT, CALL_TEXT, Step.WAITING);
|
||||
}
|
||||
break;
|
||||
// case CLICK_VIDEO_CALL:
|
||||
// if (step(Property.TEXT, VIDEO_TEXT)) {
|
||||
// Log.d(TAG, "finish, now: " + mCurrentStep);
|
||||
// Toast.makeText(this, "成功发起视频聊天", Toast.LENGTH_LONG).show();
|
||||
// }
|
||||
// break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt() {
|
||||
Log.e(TAG, "onInterrupt: ");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,4 +290,572 @@ public class SelectToSpeakService extends AccessibilityService {
|
||||
Log.e(TAG, "onServiceConnected: ");
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 打开微信
|
||||
*/
|
||||
private void launchWeChat() {
|
||||
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) {
|
||||
Log.e(TAG, "launchWeChat: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean step(Property type, String text, Step nextStep) {
|
||||
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
|
||||
if (node != null) {
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Log.e(TAG, "step: rect = " + rect);
|
||||
if (rect.left < 0 || rect.top < 0 || rect.right < 0 || rect.bottom < 0) {
|
||||
return false;
|
||||
}
|
||||
clickNode(node);
|
||||
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = nextStep;
|
||||
Log.e(TAG, "step: next: " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 2025/2/8 先把通讯录点击的换成node
|
||||
private boolean stepHome(Property type, String text, Step nextStep) {
|
||||
AccessibilityNodeInfo node = findNode(getWindows(), type, text);
|
||||
if (node != null) {
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Log.e(TAG, "step: rect = " + rect);
|
||||
if (rect.left < 0 || rect.top < 0 || rect.right < 0 || rect.bottom < 0) {
|
||||
return false;
|
||||
}
|
||||
clickNode(node);
|
||||
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = nextStep;
|
||||
Log.e(TAG, "step: next: " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private void touchContact() {
|
||||
boolean successful = clickByPoint(268, 1440);
|
||||
if (successful) {
|
||||
mCurrentStep = Step.FIND_TAG;
|
||||
} else {
|
||||
mCurrentStep = Step.WAITING;
|
||||
Toaster.show("点击失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findSearchContact(Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf");
|
||||
Log.e(TAG, "findSearchContact: " + nodeInfos);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
return optional.isPresent();
|
||||
}
|
||||
|
||||
private void findSearchContact(Property type, String text, Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/odf");
|
||||
Log.e(TAG, "findSearchContact: " + nodeInfos);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
if (optional.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = optional.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = nextStep;
|
||||
} else {
|
||||
Toaster.show("没有找到联系人");
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
//see https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction#ACTION_IME_ENTER
|
||||
nodeInfo.performAction(ACTION_IME_ENTER_ID);
|
||||
}
|
||||
mCurrentStep = nextStep;
|
||||
} else {
|
||||
Toaster.show("没有找到搜索框");
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
}
|
||||
|
||||
private void clickViewById(String id, Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
if (optional.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = optional.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = nextStep;
|
||||
} else {
|
||||
Toaster.show("没有找到搜索按钮");
|
||||
}
|
||||
}
|
||||
|
||||
private List<AccessibilityNodeInfo> findNodesByViewId(String id) {
|
||||
List<AccessibilityNodeInfo> accessibilityNodeInfos = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(id);
|
||||
return accessibilityNodeInfos;
|
||||
}
|
||||
|
||||
private AccessibilityNodeInfo findNodeByText(AccessibilityNodeInfo root, String text) {
|
||||
if (root == null) return null;
|
||||
Log.e(TAG, "findNodeByText: getText = " + root.getText());
|
||||
Log.e(TAG, "findNodeByText: getContentDescription = " + root.getContentDescription());
|
||||
boolean found = root.getText() != null && text.contentEquals(root.getText());
|
||||
if (found) {
|
||||
return root;
|
||||
} else {
|
||||
for (int i = 0; i < root.getChildCount(); i++) {
|
||||
AccessibilityNodeInfo result = findNodeByText(root.getChild(i), text);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
root.recycle();
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean stepCallDialog(Property type, String text, Step nextStep) {
|
||||
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
|
||||
if (node != null) {
|
||||
Log.e(TAG, "stepCallDialog: isVisibleToUser: " + node.isVisibleToUser());
|
||||
if (node.isVisibleToUser()) {
|
||||
clickNode(node);
|
||||
Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = nextStep;
|
||||
Log.e(TAG, "stepCallDialog: next: " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
scrollDown();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (mFindCount == mMaxCount) {
|
||||
Log.e("stepCallDialog", "mCurrentStep: max");
|
||||
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
|
||||
mCurrentStep = Step.WAITING;
|
||||
mFindCount = 0;
|
||||
return false;
|
||||
} else {
|
||||
Log.e("stepCallDialog", "mCurrentStep: not found");
|
||||
mFindCount++;
|
||||
Log.e("stepCallDialog", "mCurrentStep: mFindCount = " + mFindCount);
|
||||
scrollDown();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stepHome(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
|
||||
if (node != null) {
|
||||
clickNode(node);
|
||||
Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.CLICK_QUICK_WECHAT_CALL;
|
||||
Log.e(TAG, "stepHome: next: " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
mCurrentStep = Step.CLICK_SEARCH;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private int mFindCount = 0;
|
||||
private int mMaxCount = 5;
|
||||
|
||||
|
||||
private boolean findContact(Property type, String text, Step nextStep) {
|
||||
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
|
||||
if (node != null) {
|
||||
clickNode(node);
|
||||
Log.e("findContact", "mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = nextStep;
|
||||
Log.e("findContact", "next: " + mCurrentStep);
|
||||
mFindCount = 0;
|
||||
return true;
|
||||
} else {
|
||||
if (mFindCount == mMaxCount) {
|
||||
Log.e("findContact", "mCurrentStep: max");
|
||||
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
|
||||
mCurrentStep = Step.WAITING;
|
||||
mFindCount = 0;
|
||||
return false;
|
||||
} else {
|
||||
Log.e("findContact", "mCurrentStep: not found");
|
||||
mFindCount++;
|
||||
Log.e("findContact", "mCurrentStep: mFindCount = " + mFindCount);
|
||||
scrollDown();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private AccessibilityNodeInfo findNode(AccessibilityNodeInfo root, Property type, String text) {
|
||||
if (root == null) return null;
|
||||
// Log.v(TAG, "findNode: getPackageName = " + root.getPackageName());
|
||||
Log.v(TAG, "findNode: getText = " + root.getText());
|
||||
Log.v(TAG, "findNode: getClassName = " + root.getClassName());
|
||||
Log.v(TAG, "findNode: getContentDescription = " + root.getContentDescription());
|
||||
boolean satisfied = false;
|
||||
switch (type) {
|
||||
case TEXT:
|
||||
satisfied = root.getText() != null && text.contentEquals(root.getText());
|
||||
break;
|
||||
case CLASS_NAME:
|
||||
satisfied = root.getClassName() != null && text.contentEquals(root.getClassName());
|
||||
break;
|
||||
case DESCRIPTION:
|
||||
satisfied = root.getContentDescription() != null && text.contentEquals(root.getContentDescription());
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (satisfied) {
|
||||
return root;
|
||||
} else {
|
||||
for (int i = 0; i < root.getChildCount(); i++) {
|
||||
AccessibilityNodeInfo result = findNode(root.getChild(i), type, text);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
root.recycle();
|
||||
return null;
|
||||
}
|
||||
|
||||
private AccessibilityNodeInfo findNode(List<AccessibilityWindowInfo> windows, Property type, String text) {
|
||||
for (AccessibilityWindowInfo accessibilityWindowInfo : windows) {
|
||||
AccessibilityNodeInfo nodeInfo = findNode(accessibilityWindowInfo.getRoot(), type, text);
|
||||
if (nodeInfo != null) {
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "findNode windows: not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
private void clickNode(AccessibilityNodeInfo node) {
|
||||
try {
|
||||
Log.e(TAG, "clickNode: getText = " + node.getText());
|
||||
Log.e(TAG, "clickNode: isClickable = " + node.isClickable());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "clickNode: e = " + e.getMessage());
|
||||
}
|
||||
if (node.isClickable()) {
|
||||
//防检测机制:
|
||||
//添加随机延迟(避免高频操作)
|
||||
// handler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
//
|
||||
// }
|
||||
// }, 1000 + new Random().nextInt(100));
|
||||
boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
Log.e(TAG, "clickNode: performAction = " + performAction);
|
||||
if (!performAction) {
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Log.e(TAG, "clickNode: rect = " + rect);
|
||||
// 点击节点的中心位置
|
||||
int centerX = (rect.left + rect.right) / 2;
|
||||
int centerY = (rect.top + rect.bottom) / 2;
|
||||
Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY));
|
||||
}
|
||||
node.recycle();
|
||||
} else {
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Log.e(TAG, "clickNode: rect = " + rect);
|
||||
// 点击节点的中心位置
|
||||
int centerX = (rect.left + rect.right) / 2;
|
||||
int centerY = (rect.top + rect.bottom) / 2;
|
||||
Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY));
|
||||
}
|
||||
// else {
|
||||
// AccessibilityNodeInfo parent = node.getParent();
|
||||
// node.recycle();
|
||||
// clickNode(parent);
|
||||
// }
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private boolean stepCall(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
|
||||
if (node != null) {
|
||||
Point point = getPointtByNode(node);
|
||||
Log.e(TAG, "stepCall: " + point);
|
||||
clickByPoint(point.x, point.y);
|
||||
// clickNode(node);
|
||||
Log.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.CLICK_CALL;
|
||||
Log.e(TAG, "stepCall: next " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "stepCall: not found");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void clickVideoCall() {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/a12");
|
||||
Optional<AccessibilityNodeInfo> accessibilityNodeInfo = nodeInfos.stream().findAny();
|
||||
if (accessibilityNodeInfo.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = Step.CLICK_CALL;
|
||||
} else {
|
||||
Toaster.show("没有找到通话按钮");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stepAnswer(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getWindows(), type, text);
|
||||
if (node != null) {
|
||||
Point point = getPointtByNode(node);
|
||||
Log.e(TAG, "stepAnswer: " + point);
|
||||
clickByPoint(point.x, point.y - 50);
|
||||
clickByPoint(point.x, point.y);
|
||||
// clickNode(node);
|
||||
Log.e(TAG, "stepAnswer: mCurrentStep " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.WAITING;
|
||||
Log.e(TAG, "stepAnswer: next " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "stepAnswer: not found");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dialerHandsFree(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getWindows(), type, text);
|
||||
if (node != null) {
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Log.e(TAG, "dialerHandsFree: rect = " + rect);
|
||||
clickNode(node);
|
||||
Log.e(TAG, "dialerHandsFree: mCurrentStep: " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.WAITING;
|
||||
Log.e(TAG, "dialerHandsFree: next: " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findHandsFree(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getWindows(), type, text);
|
||||
if (node != null) {
|
||||
Log.e(TAG, "findHandsFree: true");
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "findHandsFree: false");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handsFree(Property type, String text) {
|
||||
AccessibilityNodeInfo node = findNode(getWindows(), type, text);
|
||||
if (node != null) {
|
||||
Point point = getPointtByNode(node);
|
||||
Log.e(TAG, "handsFree: " + point);
|
||||
clickByPoint(point.x, point.y - 50);
|
||||
clickByPoint(point.x, point.y);
|
||||
// clickNode(node);
|
||||
Log.e(TAG, "handsFree: mCurrentStep " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.WAITING;
|
||||
Log.e(TAG, "handsFree: next " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "handsFree: not found");
|
||||
mCurrentStep = Step.WAITING;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//根据节点信息可获得对应的x,y坐标
|
||||
static Point getPointtByNode(AccessibilityNodeInfo node) {
|
||||
if (node == null) {
|
||||
return new Point(0, 0);
|
||||
}
|
||||
Rect rect = new Rect();
|
||||
node.getBoundsInScreen(rect);
|
||||
Point point = new Point(rect.centerX(), rect.centerY());
|
||||
return point;
|
||||
}
|
||||
|
||||
//实现对(x,y)坐标进行点击操作。
|
||||
private boolean clickByPoint(int x, int y) {
|
||||
Log.e(TAG, "clickByNode: x = " + x);
|
||||
Log.e(TAG, "clickByNode: y = " + y);
|
||||
Point point = new Point(x, y);
|
||||
Path path = new Path();
|
||||
path.moveTo(point.x, point.y);
|
||||
GestureDescription.Builder builder = new GestureDescription.Builder();
|
||||
//防检测机制:
|
||||
//添加随机延迟(避免高频操作)
|
||||
builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200 + new Random().nextInt(100)));
|
||||
GestureDescription gesture = builder.build();
|
||||
boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() {
|
||||
@Override
|
||||
public void onCompleted(GestureDescription gestureDescription) {
|
||||
super.onCompleted(gestureDescription);
|
||||
Log.e("clickByNode", "onCompleted: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(GestureDescription gestureDescription) {
|
||||
super.onCancelled(gestureDescription);
|
||||
Log.e("clickByNode", "onCompleted: ");
|
||||
}
|
||||
}, 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; // 屏幕高度(像素)
|
||||
float density = dm.density; // 屏幕密度(0.75 / 1.0 / 1.5)
|
||||
int densityDpi = dm.densityDpi; // 屏幕密度dpi(120 / 160 / 240)
|
||||
// 屏幕宽度算法:屏幕宽度(像素)/屏幕密度
|
||||
// int screenWidth = (int) (width / density); // 屏幕宽度(dp)
|
||||
// int screenHeight = (int) (height / density);// 屏幕高度(dp)
|
||||
Log.e(TAG, "scrollScreen: screenWidth = " + width);
|
||||
Log.e(TAG, "scrollScreen: screenHeight = " + height);
|
||||
int center_X = width / 2;
|
||||
int center_Y = height / 2;
|
||||
Log.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);
|
||||
Log.d("scrollScreen", "dispatchGesture ScrollUp onCompleted.");
|
||||
path.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(GestureDescription gestureDescription) {
|
||||
super.onCancelled(gestureDescription);
|
||||
Log.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_CONTACT,
|
||||
|
||||
FIND_CONTACT,
|
||||
//通讯录页面点击标签
|
||||
FIND_TAG,
|
||||
//点击对应的标签名
|
||||
CLICK_TAG,
|
||||
|
||||
CLICK_NAME,
|
||||
CLICK_INFO,
|
||||
|
||||
CLICK_CALL,
|
||||
CLICK_VIDEO_CALL;
|
||||
|
||||
private Step next() {
|
||||
return values()[(this.ordinal() + 1) % values().length];
|
||||
}
|
||||
}
|
||||
|
||||
private enum Property {
|
||||
TEXT,
|
||||
CLASS_NAME,
|
||||
DESCRIPTION
|
||||
}
|
||||
|
||||
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 SettingBroadcastReceiver mSettingBroadcastReceiver;
|
||||
|
||||
private void registerSettingReceiver() {
|
||||
if (mSettingBroadcastReceiver == null) {
|
||||
mSettingBroadcastReceiver = new SettingBroadcastReceiver();
|
||||
}
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(SETTING_CALL_TYPE_ACTION);
|
||||
filter.addAction(SETTING_AUTOMATIC_ANSWER_ACTION);
|
||||
registerReceiver(mSettingBroadcastReceiver, filter);
|
||||
}
|
||||
|
||||
private class SettingBroadcastReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Log.e("SettingReceiver", "onReceive: " + action);
|
||||
if (TextUtils.isEmpty(action)) return;
|
||||
switch (action) {
|
||||
case SETTING_CALL_TYPE_ACTION:
|
||||
int callType = intent.getIntExtra("call_type", TYPE_VOICE);
|
||||
mCallType = callType;
|
||||
Log.e("SettingReceiver", "onReceive: callType = " + callType);
|
||||
break;
|
||||
case SETTING_AUTOMATIC_ANSWER_ACTION:
|
||||
boolean autoAnswer = intent.getBooleanExtra("auto_answer", false);
|
||||
mAutoAccept = autoAnswer;
|
||||
Log.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.ttstd.elderlyassistant.activity.main;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.nfc.NdefMessage;
|
||||
import android.nfc.NdefRecord;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.NfcManager;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.MifareClassic;
|
||||
import android.nfc.tech.Ndef;
|
||||
import android.nfc.tech.NfcF;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
@@ -19,19 +16,25 @@ import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.android.accessibility.selecttospeak.SelectToSpeakService;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.ttstd.elderlyassistant.R;
|
||||
import com.ttstd.elderlyassistant.activity.nfc.NfcActivity;
|
||||
import com.ttstd.elderlyassistant.bean.WechatContact;
|
||||
import com.ttstd.elderlyassistant.databinding.ActivityMainBinding;
|
||||
import com.ttstd.elderlyassistant.utils.AccessibilityUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private ActivityMainBinding mBinding;
|
||||
|
||||
private NfcManager mNfcManager;
|
||||
private NfcAdapter mNfcAdapter;
|
||||
private Tag mTag;
|
||||
private PendingIntent pendingIntent;
|
||||
private PendingIntent mPendingIntent;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -40,47 +43,50 @@ public class MainActivity extends AppCompatActivity {
|
||||
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
|
||||
mBinding.setClick(new BtnClick());
|
||||
|
||||
|
||||
mNfcManager = (NfcManager) getSystemService(Context.NFC_SERVICE);
|
||||
if (mNfcManager == null) {
|
||||
// 设备不支持 NFC
|
||||
Toast.makeText(this, "设备不支持 NFC", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
mNfcAdapter = mNfcManager.getDefaultAdapter();
|
||||
// 获取NFC适配器实例
|
||||
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
if (mNfcAdapter == null) {
|
||||
// 设备不支持 NFC
|
||||
Toast.makeText(this, "设备不支持 NFC", Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, "设备不支持NFC", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 PendingIntent 捕获 NFC 事件
|
||||
pendingIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
Log.e(TAG, "onStart: ");
|
||||
|
||||
//此处adapter需要重新获取,否则无法获取message
|
||||
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
//一旦截获NFC消息,就会通过PendingIntent调用窗口
|
||||
mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
Log.e(TAG, "onResume: ");
|
||||
|
||||
// 启用前台调度,实时监听NFC标签
|
||||
if (mNfcAdapter != null) {
|
||||
// 指定处理 TECH_DISCOVERED 类型标签
|
||||
IntentFilter[] filters = new IntentFilter[]{
|
||||
new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)
|
||||
};
|
||||
String[][] techLists = new String[][]{
|
||||
{NfcF.class.getName(), MifareClassic.class.getName()}
|
||||
};
|
||||
mNfcAdapter.enableForegroundDispatch(this, pendingIntent, filters, techLists);
|
||||
mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
|
||||
}
|
||||
|
||||
if (AccessibilityUtils.isAccessibilitySettingsOn(MainActivity.this)) {
|
||||
mBinding.button.setVisibility(View.GONE);
|
||||
} else {
|
||||
mBinding.button.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
Log.e(TAG, "onPause: ");
|
||||
|
||||
// 禁用前台调度,减少资源消耗
|
||||
if (mNfcAdapter != null) {
|
||||
mNfcAdapter.disableForegroundDispatch(this);
|
||||
}
|
||||
@@ -89,39 +95,98 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
Log.e(TAG, "onNewIntent: ");
|
||||
if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
|
||||
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
if (tag != null) {
|
||||
// 获取 NFC ID (16 进制格式)
|
||||
byte[] idBytes = tag.getId();
|
||||
String tagId = bytesToHex(idBytes);
|
||||
|
||||
// 读取 NDEF 数据(如为文本标签)
|
||||
Ndef ndef = Ndef.get(tag);
|
||||
if (ndef != null) {
|
||||
NdefMessage ndefMessage = ndef.getCachedNdefMessage();
|
||||
if (ndefMessage != null) {
|
||||
String payload = new String(ndefMessage.getRecords()[0].getPayload());
|
||||
Log.e(TAG, "onNewIntent: payload = " + payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
String action = intent.getAction();
|
||||
Log.e(TAG, "onNewIntent: action = " + action);
|
||||
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
|
||||
//1.获取Tag对象
|
||||
Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
//2.获取Ndef的实例
|
||||
Ndef ndef = Ndef.get(detectedTag);
|
||||
Log.e(TAG, "onNewIntent: type = " + ndef.getType() + " MaxSize = " + ndef.getMaxSize());
|
||||
readNfcTag(intent);
|
||||
}
|
||||
}
|
||||
|
||||
// 字节数组转 Hex 字符串
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X", b));
|
||||
/**
|
||||
* 读取NFC标签文本数据
|
||||
*/
|
||||
private void readNfcTag(Intent intent) {
|
||||
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
|
||||
NfcAdapter.EXTRA_NDEF_MESSAGES);
|
||||
NdefMessage msgs[] = null;
|
||||
int contentSize = 0;
|
||||
if (rawMsgs != null) {
|
||||
msgs = new NdefMessage[rawMsgs.length];
|
||||
for (int i = 0; i < rawMsgs.length; i++) {
|
||||
msgs[i] = (NdefMessage) rawMsgs[i];
|
||||
contentSize += msgs[i].toByteArray().length;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (msgs != null) {
|
||||
NdefRecord record = msgs[0].getRecords()[0];
|
||||
String textRecord = parseTextRecord(record);
|
||||
Log.e(TAG, "readNfcTag: textRecord = " + textRecord);
|
||||
Log.e(TAG, "readNfcTag: contentSize = " + contentSize);
|
||||
Gson gson = new Gson();
|
||||
Type type = new TypeToken<WechatContact>() {
|
||||
}.getType();
|
||||
try {
|
||||
WechatContact contact = gson.fromJson(textRecord, type);
|
||||
Intent accessibilityIntent = new Intent(MainActivity.this, SelectToSpeakService.class);
|
||||
accessibilityIntent.putExtra("WechatInfo", contact);
|
||||
startService(accessibilityIntent);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "resolveIntent: " + e.getMessage());
|
||||
Toaster.showLong("NFC数据读取失败,请检查配置是否正确");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析NDEF文本数据,从第三个字节开始,后面的文本数据
|
||||
*
|
||||
* @param ndefRecord
|
||||
* @return
|
||||
*/
|
||||
public static String parseTextRecord(NdefRecord ndefRecord) {
|
||||
/**
|
||||
* 判断数据是否为NDEF格式
|
||||
*/
|
||||
//判断TNF
|
||||
if (ndefRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
|
||||
return null;
|
||||
}
|
||||
//判断可变的长度的类型
|
||||
if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
//获得字节数组,然后进行分析
|
||||
byte[] payload = ndefRecord.getPayload();
|
||||
//下面开始NDEF文本数据第一个字节,状态字节
|
||||
//判断文本是基于UTF-8还是UTF-16的,取第一个字节"位与"上16进制的80,16进制的80也就是最高位是1,
|
||||
//其他位都是0,所以进行"位与"运算后就会保留最高位
|
||||
String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";
|
||||
//3f最高两位是0,第六位是1,所以进行"位与"运算后获得第六位
|
||||
int languageCodeLength = payload[0] & 0x3f;
|
||||
//下面开始NDEF文本数据第二个字节,语言编码
|
||||
//获得语言编码
|
||||
String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
|
||||
//下面开始NDEF文本数据后面的字节,解析出文本
|
||||
String textRecord = new String(payload, languageCodeLength + 1,
|
||||
payload.length - languageCodeLength - 1, textEncoding);
|
||||
return textRecord;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void wirteTag(View view) {
|
||||
startActivity(new Intent(MainActivity.this, NfcActivity.class));
|
||||
public void openAccessibility(View view) {
|
||||
AccessibilityUtils.openAccessibilitySettings(MainActivity.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,21 @@ import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.ttstd.elderlyassistant.R;
|
||||
import com.ttstd.elderlyassistant.bean.Contact;
|
||||
import com.ttstd.elderlyassistant.bean.WechatContact;
|
||||
import com.ttstd.elderlyassistant.databinding.ActivityNfcBinding;
|
||||
import com.ttstd.elderlyassistant.gson.GsonUtils;
|
||||
import com.ttstd.elderlyassistant.utils.NdefUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.ttstd.elderlyassistant.gson.GsonUtils;
|
||||
|
||||
public class NfcActivity extends AppCompatActivity {
|
||||
private static final String TAG = "NfcActivity";
|
||||
|
||||
@@ -60,20 +64,45 @@ public class NfcActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
getMessageFromIntent(intent);
|
||||
|
||||
resolveIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
getMessageFromIntent(intent);
|
||||
resolveIntent(intent);
|
||||
}
|
||||
|
||||
private void getMessageFromIntent(Intent intent) {
|
||||
private void resolveIntent(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Log.e(TAG, "getMessageFromIntent: action = " + action);
|
||||
Log.e(TAG, "resolveIntent: action = " + action);
|
||||
if (TextUtils.isEmpty(action)) return;
|
||||
switch (action) {
|
||||
case NfcAdapter.ACTION_TAG_DISCOVERED:
|
||||
case NfcAdapter.ACTION_TECH_DISCOVERED:
|
||||
mTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
|
||||
NdefMessage msg = null;
|
||||
if (rawMsgs != null && rawMsgs.length > 0) {
|
||||
msg = (NdefMessage) rawMsgs[0];
|
||||
}
|
||||
List<String> results = NdefUtils.ndefMessageToList(msg);
|
||||
Log.e(TAG, "resolveIntent: " + results);
|
||||
if (!results.isEmpty()) {
|
||||
String jsonString = results.get(0);
|
||||
Gson gson = new Gson();
|
||||
Type type = new TypeToken<WechatContact>() {
|
||||
}.getType();
|
||||
try {
|
||||
WechatContact contact = gson.fromJson(jsonString, type);
|
||||
mBinding.setWechatContact(contact);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "resolveIntent: " + e.getMessage());
|
||||
Toaster.showLong("NFC数据读取失败,请检查配置是否正确");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NfcAdapter.ACTION_NDEF_DISCOVERED:
|
||||
Parcelable[] rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
|
||||
if (rawMessages != null) {
|
||||
@@ -85,38 +114,9 @@ public class NfcActivity extends AppCompatActivity {
|
||||
processNdefMessages(messages);
|
||||
}
|
||||
break;
|
||||
case NfcAdapter.ACTION_TECH_DISCOVERED:
|
||||
mTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
if (mTag != null) {
|
||||
// 获取 NFC ID (16 进制格式)
|
||||
byte[] idBytes = mTag.getId();
|
||||
String tagId = bytesToHex(idBytes);
|
||||
Log.e(TAG, "getMessageFromIntent: tagId = " + tagId);
|
||||
// 读取 NDEF 数据(如为文本标签)
|
||||
Ndef ndef = Ndef.get(mTag);
|
||||
if (ndef != null) {
|
||||
NdefMessage ndefMessage = ndef.getCachedNdefMessage();
|
||||
if (ndefMessage != null) {
|
||||
String payload = new String(ndefMessage.getRecords()[0].getPayload());
|
||||
Log.e(TAG, "getMessageFromIntent: payload = " + payload);
|
||||
} else {
|
||||
Log.e(TAG, "getMessageFromIntent: ndefMessage is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 字节数组转 Hex 字符串
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void processNdefMessages(NdefMessage[] messages) {
|
||||
for (NdefMessage message : messages) {
|
||||
for (NdefRecord record : message.getRecords()) {
|
||||
@@ -131,6 +131,7 @@ public class NfcActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
public void writeTag(String text, Tag tag) {
|
||||
Log.e(TAG, "writeTag: text = " + text);
|
||||
try {
|
||||
NdefRecord record = createTextRecord(text, Locale.ENGLISH);
|
||||
NdefMessage message = new NdefMessage(new NdefRecord[]{record});
|
||||
@@ -176,8 +177,8 @@ public class NfcActivity extends AppCompatActivity {
|
||||
Toaster.show("请输入联系人手机号");
|
||||
return;
|
||||
}
|
||||
Contact contact = new Contact(name, phone);
|
||||
writeTag(GsonUtils.toJSONString(contact), mTag);
|
||||
WechatContact wechatContact = new WechatContact(name, phone);
|
||||
writeTag(GsonUtils.toJSONString(wechatContact), mTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package com.ttstd.elderlyassistant.base;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
public class BaseApplication extends Application {
|
||||
private static final String TAG = "BaseApplication";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Toaster.init(this);
|
||||
String rootDir = MMKV.initialize(this);
|
||||
Log.e(TAG, "mmkv root: " + rootDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
package com.ttstd.elderlyassistant.bean;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Contact implements Serializable {
|
||||
public class WechatContact implements Serializable {
|
||||
private static final long serialVersionUID = -5570426400839799887L;
|
||||
|
||||
String name;
|
||||
String phone;
|
||||
String tag;
|
||||
public String name;
|
||||
public String phone;
|
||||
public String tag;
|
||||
/**
|
||||
* 0 电话 1 短信 2 微信语音 3 微信视频
|
||||
*/
|
||||
int defaultOperation;
|
||||
int operation;
|
||||
int version;
|
||||
|
||||
|
||||
public Contact() {
|
||||
public WechatContact() {
|
||||
|
||||
}
|
||||
|
||||
public Contact(String name, String phone) {
|
||||
public WechatContact(String name, String phone) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
this.defaultOperation = 3;
|
||||
this.operation = 3;
|
||||
this.version = 1;
|
||||
}
|
||||
|
||||
@@ -50,4 +55,10 @@ public class Contact implements Serializable {
|
||||
public void setTag(String tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ package com.ttstd.elderlyassistant.config;
|
||||
|
||||
public class CommonConfig {
|
||||
public static final String MMKV_ID = "InterProcessKV";
|
||||
|
||||
public static final String WECHAT_CALL_AUTO_ACCEPT_KEY ="wechat_call_auto_accept";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ttstd.elderlyassistant.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.accessibility.selecttospeak.SelectToSpeakService;
|
||||
|
||||
public class AccessibilityUtils {
|
||||
private static final String TAG = "AccessibilityUtils";
|
||||
|
||||
public static boolean isAccessibilitySettingsOn(Context context) {
|
||||
int accessibilityEnabled = 0;
|
||||
final String service = context.getPackageName() + "/" + SelectToSpeakService.class.getCanonicalName();
|
||||
try {
|
||||
accessibilityEnabled = Settings.Secure.getInt(context.getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
|
||||
} catch (Settings.SettingNotFoundException e) {
|
||||
Log.e(TAG, "Error finding setting, default accessibility to not found: " + e.getMessage());
|
||||
}
|
||||
TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
|
||||
if (accessibilityEnabled == 1) {
|
||||
String settingValue = Settings.Secure.getString(context.getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
|
||||
if (settingValue != null) {
|
||||
mStringColonSplitter.setString(settingValue);
|
||||
while (mStringColonSplitter.hasNext()) {
|
||||
String accessibilityService = mStringColonSplitter.next();
|
||||
if (accessibilityService.equalsIgnoreCase(service)) {
|
||||
Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void openAccessibilitySettings(Context context) {
|
||||
Toast.makeText(context, "请在无障碍服务中打开 - 老人助理快捷服务", Toast.LENGTH_LONG).show();
|
||||
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
|
||||
String str = context.getPackageName() + "/" + SelectToSpeakService.class.getName();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(":settings:fragment_args_key", str);
|
||||
intent.putExtra(":settings:fragment_args_key", str);
|
||||
intent.putExtra(":settings:show_fragment_args", bundle);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.ttstd.elderlyassistant.utils;
|
||||
|
||||
import android.nfc.NdefMessage;
|
||||
import android.nfc.NdefRecord;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class NdefUtils {
|
||||
|
||||
public static List<String> ndefMessageToList(NdefMessage message) {
|
||||
List<String> stringList = new ArrayList<>();
|
||||
if (message == null) return stringList;
|
||||
for (NdefRecord record : message.getRecords()) {
|
||||
String recordStr = parseNdefRecord(record);
|
||||
stringList.add(recordStr);
|
||||
}
|
||||
return stringList;
|
||||
}
|
||||
|
||||
// 将 NdefMessage 转换为字符串
|
||||
public static String ndefMessageToString(NdefMessage message) {
|
||||
if (message == null) return null;
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (NdefRecord record : message.getRecords()) {
|
||||
String recordStr = parseNdefRecord(record);
|
||||
if (recordStr != null) {
|
||||
result.append(recordStr).append("\n");
|
||||
}
|
||||
}
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
// 解析单个 NdefRecord
|
||||
private static String parseNdefRecord(NdefRecord record) {
|
||||
if (record == null) return null;
|
||||
|
||||
// 处理文本类型记录
|
||||
if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN &&
|
||||
Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)) {
|
||||
return parseTextRecord(record);
|
||||
}
|
||||
// 处理 URI 类型记录
|
||||
else if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN &&
|
||||
Arrays.equals(record.getType(), NdefRecord.RTD_URI)) {
|
||||
return parseUriRecord(record);
|
||||
}
|
||||
// 其他类型(如 MIME 类型)可在此扩展
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析文本记录
|
||||
private static String parseTextRecord(NdefRecord record) {
|
||||
byte[] payload = record.getPayload();
|
||||
// 获取编码类型(UTF-8 或 UTF-16)
|
||||
Charset encoding = ((payload[0] & 0x80) == 0) ?
|
||||
StandardCharsets.UTF_8 : StandardCharsets.UTF_16;
|
||||
// 获取语言码长度(首字节低6位)
|
||||
int languageLength = payload[0] & 0x3F;
|
||||
// 提取文本内容(跳过语言码)
|
||||
return new String(payload, languageLength + 1, payload.length - languageLength - 1, encoding);
|
||||
}
|
||||
|
||||
// 解析 URI 记录
|
||||
private static String parseUriRecord(NdefRecord record) {
|
||||
byte[] payload = record.getPayload();
|
||||
// 获取 URI 前缀(如 https://, tel:)
|
||||
String prefix = UriPrefix.getUriPrefix(payload[0]);
|
||||
byte[] fullUri = new byte[payload.length - 1];
|
||||
System.arraycopy(payload, 1, fullUri, 0, fullUri.length);
|
||||
return prefix + new String(fullUri, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// URI 前缀映射表(简化版)
|
||||
private static class UriPrefix {
|
||||
static String getUriPrefix(byte prefixByte) {
|
||||
switch (prefixByte) {
|
||||
case 0x01:
|
||||
return "http://www.";
|
||||
case 0x02:
|
||||
return "https://www.";
|
||||
case 0x03:
|
||||
return "http://";
|
||||
case 0x04:
|
||||
return "https://";
|
||||
case 0x05:
|
||||
return "tel:";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user