From abe8a96bced8a335436ecc66f7d66832aae9b003 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Sun, 6 Jul 2025 20:28:31 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E8=BF=87nfc=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E6=95=B0=E6=8D=AE=E5=B9=B6=E6=8B=A8=E6=89=93?= =?UTF-8?q?=E7=94=B5=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 51 +- .../selecttospeak/SelectToSpeakService.java | 815 +++++++++++++++++- .../activity/main/MainActivity.java | 185 ++-- .../activity/nfc/NfcActivity.java | 79 +- .../base/BaseApplication.java | 5 + .../bean/{Contact.java => WechatContact.java} | 27 +- .../elderlyassistant/config/CommonConfig.java | 2 +- .../utils/AccessibilityUtils.java | 53 ++ .../elderlyassistant/utils/NdefUtils.java | 96 +++ app/src/main/res/layout/activity_main.xml | 6 +- app/src/main/res/layout/activity_nfc.xml | 6 + app/src/main/res/values/strings.xml | 4 +- .../res/xml/accessibility_service_config.xml | 2 +- app/src/main/res/xml/filter_nfc.xml | 21 + app/src/main/res/xml/nfc_tech_filter.xml | 5 - app/src/main/res/xml/provider_paths.xml | 4 + 16 files changed, 1217 insertions(+), 144 deletions(-) rename app/src/main/java/com/ttstd/elderlyassistant/bean/{Contact.java => WechatContact.java} (58%) create mode 100644 app/src/main/java/com/ttstd/elderlyassistant/utils/AccessibilityUtils.java create mode 100644 app/src/main/java/com/ttstd/elderlyassistant/utils/NdefUtils.java create mode 100644 app/src/main/res/xml/filter_nfc.xml delete mode 100644 app/src/main/res/xml/nfc_tech_filter.xml create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da6e42c..68a9915 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,39 +20,36 @@ android:theme="@style/AppTheme"> + android:exported="true" + android:launchMode="singleTop"> - - - - - - - - - - - - - - - + android:launchMode="singleTask" + android:permission="android.permission.NFC"> + + + + + + + + + + + + + - - - + + + + diff --git a/app/src/main/java/com/google/android/accessibility/selecttospeak/SelectToSpeakService.java b/app/src/main/java/com/google/android/accessibility/selecttospeak/SelectToSpeakService.java index 28f27e6..3a9ca2d 100644 --- a/app/src/main/java/com/google/android/accessibility/selecttospeak/SelectToSpeakService.java +++ b/app/src/main/java/com/google/android/accessibility/selecttospeak/SelectToSpeakService.java @@ -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() { + @Override + public void subscribe(@NonNull ObservableEmitter emitter) throws Throwable { + mAccessibilityEventCallback = emitter::onNext; + } + }).throttleLast(WAIT_TIME, TimeUnit.MILLISECONDS) + .subscribe(new Consumer() { + @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 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf"); + Log.e(TAG, "findSearchContact: " + nodeInfos); + Optional optional = nodeInfos.stream().findAny(); + return optional.isPresent(); + } + + private void findSearchContact(Property type, String text, Step nextStep) { + List nodeInfos = findNodesByViewId("com.tencent.mm:id/odf"); + Log.e(TAG, "findSearchContact: " + nodeInfos); + Optional 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/d98"); + Optional 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 nodeInfos = findNodesByViewId(id); + Optional optional = nodeInfos.stream().findAny(); + if (optional.isPresent()) { + AccessibilityNodeInfo nodeInfo = optional.get(); + clickNode(nodeInfo); + mCurrentStep = nextStep; + } else { + Toaster.show("没有找到搜索按钮"); + } + } + + private List findNodesByViewId(String id) { + List 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 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/a12"); + Optional 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: + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/elderlyassistant/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/elderlyassistant/activity/main/MainActivity.java index d0f7aeb..ae290da 100644 --- a/app/src/main/java/com/ttstd/elderlyassistant/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/elderlyassistant/activity/main/MainActivity.java @@ -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() { + }.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); } } } diff --git a/app/src/main/java/com/ttstd/elderlyassistant/activity/nfc/NfcActivity.java b/app/src/main/java/com/ttstd/elderlyassistant/activity/nfc/NfcActivity.java index 8b260dd..96f6da5 100644 --- a/app/src/main/java/com/ttstd/elderlyassistant/activity/nfc/NfcActivity.java +++ b/app/src/main/java/com/ttstd/elderlyassistant/activity/nfc/NfcActivity.java @@ -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 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() { + }.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); } } } diff --git a/app/src/main/java/com/ttstd/elderlyassistant/base/BaseApplication.java b/app/src/main/java/com/ttstd/elderlyassistant/base/BaseApplication.java index 460d4ba..3e57cab 100644 --- a/app/src/main/java/com/ttstd/elderlyassistant/base/BaseApplication.java +++ b/app/src/main/java/com/ttstd/elderlyassistant/base/BaseApplication.java @@ -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 diff --git a/app/src/main/java/com/ttstd/elderlyassistant/bean/Contact.java b/app/src/main/java/com/ttstd/elderlyassistant/bean/WechatContact.java similarity index 58% rename from app/src/main/java/com/ttstd/elderlyassistant/bean/Contact.java rename to app/src/main/java/com/ttstd/elderlyassistant/bean/WechatContact.java index d3f443e..412445c 100644 --- a/app/src/main/java/com/ttstd/elderlyassistant/bean/Contact.java +++ b/app/src/main/java/com/ttstd/elderlyassistant/bean/WechatContact.java @@ -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(); + } } diff --git a/app/src/main/java/com/ttstd/elderlyassistant/config/CommonConfig.java b/app/src/main/java/com/ttstd/elderlyassistant/config/CommonConfig.java index d95bf93..e9d0e42 100644 --- a/app/src/main/java/com/ttstd/elderlyassistant/config/CommonConfig.java +++ b/app/src/main/java/com/ttstd/elderlyassistant/config/CommonConfig.java @@ -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"; } diff --git a/app/src/main/java/com/ttstd/elderlyassistant/utils/AccessibilityUtils.java b/app/src/main/java/com/ttstd/elderlyassistant/utils/AccessibilityUtils.java new file mode 100644 index 0000000..bfcff1d --- /dev/null +++ b/app/src/main/java/com/ttstd/elderlyassistant/utils/AccessibilityUtils.java @@ -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); + } +} diff --git a/app/src/main/java/com/ttstd/elderlyassistant/utils/NdefUtils.java b/app/src/main/java/com/ttstd/elderlyassistant/utils/NdefUtils.java new file mode 100644 index 0000000..d059aa7 --- /dev/null +++ b/app/src/main/java/com/ttstd/elderlyassistant/utils/NdefUtils.java @@ -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 ndefMessageToList(NdefMessage message) { + List 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 ""; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d87d369..363b0f4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -15,12 +15,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + +