通过nfc实时读取数据并拨打电话

This commit is contained in:
2025-07-06 20:28:31 +08:00
parent 1cd1f2debd
commit abe8a96bce
16 changed files with 1217 additions and 144 deletions

View File

@@ -20,39 +20,36 @@
android:theme="@style/AppTheme">
<activity
android:name=".activity.main.MainActivity"
android:exported="true">
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <!-- 指定数据类型 -->
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<activity
android:name=".activity.nfc.NfcActivity"
android:exported="true"
android:launchMode="singleTop">
<!-- <intent-filter>-->
<!-- <action android:name="android.nfc.action.NDEF_DISCOVERED" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:mimeType="text/plain" />-->
<!-- </intent-filter>-->
<!-- <intent-filter>-->
<!-- <action android:name="android.nfc.action.TECH_DISCOVERED" />-->
<!-- </intent-filter>-->
android:launchMode="singleTask"
android:permission="android.permission.NFC">
<!-- Offer to display anything with NDEF data -->
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/filter_nfc" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="vnd.android.cursor.item/ndef_msg" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- <meta-data-->
<!-- android:name="android.nfc.action.TECH_DISCOVERED"-->
<!-- android:resource="@xml/nfc_tech_filter" />-->
</activity>
<service
@@ -71,6 +68,16 @@
android:resource="@xml/accessibility_service_config" />
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.tag.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<meta-data
android:name="design_width_in_dp"
android:value="360" />

View File

@@ -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.498.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;
}
}
//根据节点信息可获得对应的xy坐标
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;
}
//实现对xy坐标进行点击操作。
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; // 屏幕密度dpi120 / 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:
}
}
}
}

View File

@@ -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进制的8016进制的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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -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 "";
}
}
}
}

View File

@@ -15,12 +15,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::wirteTag}"
android:text="写入标签"
android:onClick="@{click::openAccessibility}"
android:text="打开无障碍权限"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -9,6 +9,10 @@
<variable
name="click"
type="com.ttstd.elderlyassistant.activity.nfc.NfcActivity.BtnClick" />
<variable
name="wechatContact"
type="com.ttstd.elderlyassistant.bean.WechatContact" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -23,6 +27,7 @@
android:ems="10"
android:hint="Name"
android:inputType="text"
android:text="@{wechatContact.name}"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
@@ -38,6 +43,7 @@
android:inputType="phone"
android:maxLines="1"
android:singleLine="true"
android:text="@{wechatContact.phone}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_name" />

View File

@@ -1,7 +1,7 @@
<resources>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">ElderlyAssistant</string>
<string name="accessibility_service_label">老人助理快捷服务</string>
<string name="accessibility_service_label">老人助理快捷服务-点击开启</string>
<string name="accessibility_service_description">老人助理一键拨打视频</string>
</resources>

View File

@@ -6,5 +6,5 @@
android:canRequestEnhancedWebAccessibility="true"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100"
android:notificationTimeout="1000"
android:packageNames="com.tencent.mm,com.android.incallui" />

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- capture anything containing NDEF -->
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
</resources>

View File

@@ -1,5 +0,0 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>