优化通过scheme直接拨号和视频,增加数据加密。优化ContactInfo数据结构。优化无障碍流程

This commit is contained in:
2025-07-18 01:46:16 +08:00
parent ea3a446768
commit f242a915bc
13 changed files with 760 additions and 225 deletions

View File

@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ttstd.elderlyassistant"> package="com.ttstd.elderlyassistant">
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- NFC权限 --> <!-- NFC权限 -->
<uses-permission android:name="android.permission.NFC" /> <!-- 声明只有带NFC的手机才可以下载 --> <uses-permission android:name="android.permission.NFC" /> <!-- 声明只有带NFC的手机才可以下载 -->
<uses-feature <uses-feature
android:name="android.hardware.nfc" android:name="android.hardware.nfc"
android:required="true" /> android:required="true" />
@@ -53,16 +57,31 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <activity-->
<!-- android:name=".activity.third_party.NfcQQMusicActivity"--> <!-- <activity -->
<!-- android:launchMode="singleTask">--> <!-- android:name=".activity.third_party.NfcQQMusicActivity" -->
<!-- <intent-filter>--> <!-- android:launchMode="singleTask"> -->
<!-- <action android:name="android.nfc.action.NDEF_DISCOVERED" />--> <!-- <intent-filter> -->
<!-- <category android:name="android.intent.category.DEFAULT" />--> <!-- <action android:name="android.nfc.action.NDEF_DISCOVERED" /> -->
<!-- &lt;!&ndash; 匹配 QQ 音乐 Scheme &ndash;&gt;--> <!-- <category android:name="android.intent.category.DEFAULT" /> -->
<!-- <data android:scheme="qqmusic" />--> <!-- &lt;!&ndash; 匹配 QQ 音乐 Scheme &ndash;&gt; -->
<!-- </intent-filter>--> <!-- <data android:scheme="qqmusic" /> -->
<!-- </activity>--> <!-- </intent-filter> -->
<!-- </activity> -->
<activity
android:name=".activity.scheme.SchemeActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<!-- 指定 Scheme 协议 -->
<data android:scheme="elderlyassistant" android:host="ttstd.com" />
</intent-filter>
</activity>
<service <service
android:name="com.google.android.accessibility.selecttospeak.SelectToSpeakService" android:name="com.google.android.accessibility.selecttospeak.SelectToSpeakService"

View File

@@ -23,7 +23,7 @@ import android.widget.Toast;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.tencent.mmkv.MMKV; import com.tencent.mmkv.MMKV;
import com.ttstd.elderlyassistant.bean.WechatContact; import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.config.CommonConfig; import com.ttstd.elderlyassistant.config.CommonConfig;
import java.util.List; import java.util.List;
@@ -39,7 +39,7 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
/** /**
* 通过微信标签最高支持8.0.498.0.50 获取不到数据 * 通过微信标签最高支持8.0.49
* 8.0.54 可以获取 * 8.0.54 可以获取
* 通过 {@link AccessibilityService#getWindows}和修改accessibility-service 配置能遍历屏幕元素 * 通过 {@link AccessibilityService#getWindows}和修改accessibility-service 配置能遍历屏幕元素
*/ */
@@ -56,8 +56,8 @@ public class SelectToSpeakService extends AccessibilityService {
private static final String TAG_TEXT = "标签"; private static final String TAG_TEXT = "标签";
private static final String PARENT_VIDEO_TEXT = "视频通话"; private static final String PARENT_VIDEO_TEXT = "视频通话";
private static final String VOICE_TEXT = "语音通话";
private static final String VIDEO_TEXT = "视频通话"; private static final String VIDEO_TEXT = "视频通话";
private static final String CALL_TEXT = "语音通话";
private static final String RECEIVE_DESCRIPTION = "接听"; private static final String RECEIVE_DESCRIPTION = "接听";
private static final String HANDS_FREE_TEXT = "扬声器已关"; private static final String HANDS_FREE_TEXT = "扬声器已关";
@@ -65,17 +65,19 @@ public class SelectToSpeakService extends AccessibilityService {
private static final String DIALER_HANDS_FREE_TEXT = "免提"; private static final String DIALER_HANDS_FREE_TEXT = "免提";
private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭"; private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭";
public static final int TYPE_VOICE = 2; public static final int TYPE_VOICE = 3;
public static final int TYPE_VIDEO = 3; public static final int TYPE_VIDEO = 4;
private static final int WAIT_TIME = 1500; private static final int WAIT_TIME = 1500;
private WechatContact mContact; private ContactInfo mContact;
private String mName = "";//微信昵称 /**
* 微信昵称
*/
private String mName = "";
private int mCallType = TYPE_VOICE; private int mCallType = TYPE_VOICE;
private Step mCurrentStep = Step.WAITING; private Procedure mCurrentProcedure = Procedure.WAITING;
private boolean mAutoAccept = false; private boolean mAutoAccept = false;
private Disposable mDisposable; private Disposable mDisposable;
@@ -92,11 +94,11 @@ public class SelectToSpeakService extends AccessibilityService {
Log.e(TAG, "onCreate: "); Log.e(TAG, "onCreate: ");
registerSettingReceiver(); registerSettingReceiver();
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false); mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
dealingAccessibilityEvent();
initDisposable();
} }
private void dealingAccessibilityEvent() { private void initDisposable() {
mDisposable = Observable.create(new ObservableOnSubscribe<AccessibilityEvent>() { mDisposable = Observable.create(new ObservableOnSubscribe<AccessibilityEvent>() {
@Override @Override
public void subscribe(@NonNull ObservableEmitter<AccessibilityEvent> emitter) throws Throwable { public void subscribe(@NonNull ObservableEmitter<AccessibilityEvent> emitter) throws Throwable {
@@ -106,23 +108,23 @@ public class SelectToSpeakService extends AccessibilityService {
.subscribe(new Consumer<AccessibilityEvent>() { .subscribe(new Consumer<AccessibilityEvent>() {
@Override @Override
public void accept(AccessibilityEvent accessibilityEvent) throws Throwable { public void accept(AccessibilityEvent accessibilityEvent) throws Throwable {
Log.e(TAG, "accept: "); Log.e(TAG, "initDisposable accept: ");
_onAccessibilityEvent(accessibilityEvent); parseAccessibilityEvent(accessibilityEvent);
} }
}); });
Log.e(TAG, "dealingAccessibilityEvent: mDisposable hash = " + mDisposable.hashCode()); Log.e(TAG, "initDisposable: mDisposable hash = " + mDisposable.hashCode());
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand: "); Log.e(TAG, "onStartCommand: ");
if (intent != null) { if (intent != null) {
mContact = (WechatContact) intent.getSerializableExtra("WechatInfo"); mContact = (ContactInfo) intent.getSerializableExtra("WechatInfo");
Log.e(TAG, "onStartCommand: wechatInfo = " + mContact); Log.e(TAG, "onStartCommand: wechatInfo = " + mContact);
if (mContact != null) { if (mContact != null) {
mName = mContact.getName(); mName = mContact.getName();
mCallType = mContact.getOperation(); mCallType = mContact.getOperation();
mCurrentStep = Step.CLICK_HOME; mCurrentProcedure = Procedure.CLICK_HOME;
launchWeChat(); launchWeChat();
} else { } else {
Toaster.showLong("没有获取到联系人数据"); Toaster.showLong("没有获取到联系人数据");
@@ -155,12 +157,12 @@ public class SelectToSpeakService extends AccessibilityService {
} }
private void checkClassName(AccessibilityEvent event) { private void checkClassName(AccessibilityEvent event) {
Log.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep); Log.e(TAG, "checkClassName: mCurrentStep = " + mCurrentProcedure);
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
String currentPackageName = event.getPackageName().toString(); String currentPackageName = event.getPackageName().toString();
String currentClassName = event.getClassName().toString(); String currentClassName = event.getClassName().toString();
switch (mCurrentStep) { switch (mCurrentProcedure) {
case WAITING: case WAITING:
if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) { if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) {
Log.e(TAG, "checkClassName: to dialer hands free"); Log.e(TAG, "checkClassName: to dialer hands free");
@@ -178,7 +180,7 @@ public class SelectToSpeakService extends AccessibilityService {
case "com.tencent.mm.plugin.account.ui.WelcomeActivity": case "com.tencent.mm.plugin.account.ui.WelcomeActivity":
case "com.tencent.mm.plugin.account.ui.LoginPasswordUI": case "com.tencent.mm.plugin.account.ui.LoginPasswordUI":
Toaster.showLong("请先登录微信"); Toaster.showLong("请先登录微信");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
break; break;
case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI": case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI":
@@ -197,20 +199,20 @@ public class SelectToSpeakService extends AccessibilityService {
* *
* @param event * @param event
*/ */
private void _onAccessibilityEvent(AccessibilityEvent event) { private void parseAccessibilityEvent(AccessibilityEvent event) {
Log.e(TAG, "_onAccessibilityEvent: " + mCurrentStep); Log.e(TAG, "parseAccessibilityEvent: " + mCurrentProcedure);
switch (mCurrentStep) { switch (mCurrentProcedure) {
case WAITING: case WAITING:
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false); mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
Log.e(TAG, "_onAccessibilityEvent: mAutoAccept = " + mAutoAccept); Log.e(TAG, "parseAccessibilityEvent: mAutoAccept = " + mAutoAccept);
if (!mAutoAccept) { if (!mAutoAccept) {
return; return;
} }
if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) { if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) {
mCurrentStep = Step.WECHAT_HANDS_FREE; mCurrentProcedure = Procedure.WECHAT_HANDS_FREE;
Toast.makeText(this, "已自动接听视频/语音", Toast.LENGTH_LONG).show(); Toast.makeText(this, "已自动接听视频/语音", Toast.LENGTH_LONG).show();
} else { } else {
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
// clickAnswer(); // clickAnswer();
} }
break; break;
@@ -221,29 +223,29 @@ public class SelectToSpeakService extends AccessibilityService {
if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) { if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) {
dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT); dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT);
} else { } else {
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
} }
break; break;
case CLICK_HOME://主页能找到直接点击进去更多 case CLICK_HOME://主页能找到直接点击进去更多
if (stepHome(Property.TEXT, mName)) { if (stepHome(Property.TEXT, mName)) {
Log.e(TAG, "_onAccessibilityEvent: not found contact in home"); Log.e(TAG, "parseAccessibilityEvent: not found contact in home");
} else { } else {
clickViewById("com.tencent.mm:id/jha", Step.CLICK_SEARCH); clickViewById("com.tencent.mm:id/jha", Procedure.CLICK_SEARCH);
// step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH); // step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH);
} }
break; break;
case CLICK_SEARCH: case CLICK_SEARCH:
putString(mName, Step.CLICK_SEARCH_CONTACT); putString(mName, Procedure.CLICK_SEARCH_CONTACT);
break; break;
case CLICK_SEARCH_CONTACT: case CLICK_SEARCH_CONTACT:
if (findSearchContact(Step.FIND_CONTACT)) { if (findSearchContact(Procedure.FIND_CONTACT)) {
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); findSearchContact(Property.TEXT, mName, Procedure.CLICK_QUICK_WECHAT_CALL);
} else { } else {
Toaster.show("没有找到联系人"); Toaster.show("没有找到联系人");
} }
break; break;
case CLICK_QUICK_WECHAT_CALL://点击更多页面 case CLICK_QUICK_WECHAT_CALL://点击更多页面
clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET); clickViewById("com.tencent.mm:id/bjz", Procedure.CLICK_TARGET);
// step(Property.DESCRIPTION, MORE_NAME, Step.CLICK_TARGET); // step(Property.DESCRIPTION, MORE_NAME, Step.CLICK_TARGET);
break; break;
case CLICK_TARGET://点击视频通话 case CLICK_TARGET://点击视频通话
@@ -252,40 +254,34 @@ public class SelectToSpeakService extends AccessibilityService {
break; break;
case CLICK_CONTACT://进入通讯录界面 case CLICK_CONTACT://进入通讯录界面
if (stepHome(Property.TEXT, CONTACT_TEXT, Step.FIND_TAG)) { if (stepHome(Property.TEXT, CONTACT_TEXT, Procedure.FIND_TAG)) {
Log.e(TAG, "_onAccessibilityEvent: enter contact"); Log.e(TAG, "parseAccessibilityEvent: enter contact");
} else { } else {
touchContact(); touchContact();
} }
break; break;
case FIND_CONTACT://模拟滑动找到联系人 case FIND_CONTACT://模拟滑动找到联系人
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); findSearchContact(Property.TEXT, mName, Procedure.CLICK_QUICK_WECHAT_CALL);
break; break;
case FIND_TAG: case FIND_TAG:
step(Property.TEXT, TAG_TEXT, Step.CLICK_TAG); step(Property.TEXT, TAG_TEXT, Procedure.CLICK_TAG);
break; break;
case CLICK_TAG: case CLICK_TAG:
break; break;
case CLICK_NAME://点击item case CLICK_NAME://点击item
findContact(Property.TEXT, mName, Step.CLICK_INFO); findContact(Property.TEXT, mName, Procedure.CLICK_INFO);
break; break;
case CLICK_INFO://进入个人信息页面 case CLICK_INFO://进入个人信息页面
stepCallDialog(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL); stepCallDialog(Property.TEXT, DIALER_TEXT, Procedure.CLICK_CALL);
break; break;
case CLICK_CALL://打视频或者电话 case CLICK_CALL://打视频或者电话
if (mCallType == TYPE_VIDEO) { if (mCallType == TYPE_VIDEO) {
step(Property.TEXT, VIDEO_TEXT, Step.WAITING); step(Property.TEXT, VIDEO_TEXT, Procedure.WAITING);
} else if (mCallType == TYPE_VOICE) { } else if (mCallType == TYPE_VOICE) {
step(Property.TEXT, CALL_TEXT, Step.WAITING); step(Property.TEXT, VOICE_TEXT, Procedure.WAITING);
} }
break; 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: default:
} }
} }
@@ -320,7 +316,7 @@ public class SelectToSpeakService extends AccessibilityService {
} }
} }
private boolean step(Property type, String text, Step nextStep) { private boolean step(Property type, String text, Procedure nextProcedure) {
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
if (node != null) { if (node != null) {
Rect rect = new Rect(); Rect rect = new Rect();
@@ -330,9 +326,9 @@ public class SelectToSpeakService extends AccessibilityService {
return false; return false;
} }
clickNode(node); clickNode(node);
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done"); Log.e(TAG, "step: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
Log.e(TAG, "step: next: " + mCurrentStep); Log.e(TAG, "step: next: " + mCurrentProcedure);
return true; return true;
} else { } else {
return false; return false;
@@ -340,7 +336,7 @@ public class SelectToSpeakService extends AccessibilityService {
} }
// TODO: 2025/2/8 先把通讯录点击的换成node // TODO: 2025/2/8 先把通讯录点击的换成node
private boolean stepHome(Property type, String text, Step nextStep) { private boolean stepHome(Property type, String text, Procedure nextProcedure) {
AccessibilityNodeInfo node = findNode(getWindows(), type, text); AccessibilityNodeInfo node = findNode(getWindows(), type, text);
if (node != null) { if (node != null) {
Rect rect = new Rect(); Rect rect = new Rect();
@@ -350,9 +346,9 @@ public class SelectToSpeakService extends AccessibilityService {
return false; return false;
} }
clickNode(node); clickNode(node);
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done"); Log.e(TAG, "step: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
Log.e(TAG, "step: next: " + mCurrentStep); Log.e(TAG, "step: next: " + mCurrentProcedure);
return true; return true;
} else { } else {
return false; return false;
@@ -363,35 +359,35 @@ public class SelectToSpeakService extends AccessibilityService {
private void touchContact() { private void touchContact() {
boolean successful = clickByPoint(268, 1440); boolean successful = clickByPoint(268, 1440);
if (successful) { if (successful) {
mCurrentStep = Step.FIND_TAG; mCurrentProcedure = Procedure.FIND_TAG;
} else { } else {
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
Toaster.show("点击失败,请重试"); Toaster.show("点击失败,请重试");
} }
} }
private boolean findSearchContact(Step nextStep) { private boolean findSearchContact(Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf"); List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf");
Log.e(TAG, "findSearchContact: " + nodeInfos); Log.e(TAG, "findSearchContact: " + nodeInfos);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
return optional.isPresent(); return optional.isPresent();
} }
private void findSearchContact(Property type, String text, Step nextStep) { private void findSearchContact(Property type, String text, Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/odf"); List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/odf");
Log.e(TAG, "findSearchContact: " + nodeInfos); Log.e(TAG, "findSearchContact: " + nodeInfos);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
if (optional.isPresent()) { if (optional.isPresent()) {
AccessibilityNodeInfo nodeInfo = optional.get(); AccessibilityNodeInfo nodeInfo = optional.get();
clickNode(nodeInfo); clickNode(nodeInfo);
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
} else { } else {
Toaster.show("没有找到联系人"); Toaster.show("没有找到联系人");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
} }
} }
private void putString(String text, Step nextStep) { private void putString(String text, Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/d98"); List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/d98");
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
if (optional.isPresent()) { if (optional.isPresent()) {
@@ -405,20 +401,20 @@ public class SelectToSpeakService extends AccessibilityService {
//see https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction#ACTION_IME_ENTER //see https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction#ACTION_IME_ENTER
nodeInfo.performAction(ACTION_IME_ENTER_ID); nodeInfo.performAction(ACTION_IME_ENTER_ID);
} }
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
} else { } else {
Toaster.show("没有找到搜索框"); Toaster.show("没有找到搜索框");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
} }
} }
private void clickViewById(String id, Step nextStep) { private void clickViewById(String id, Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id); List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny(); Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
if (optional.isPresent()) { if (optional.isPresent()) {
AccessibilityNodeInfo nodeInfo = optional.get(); AccessibilityNodeInfo nodeInfo = optional.get();
clickNode(nodeInfo); clickNode(nodeInfo);
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
} else { } else {
Toaster.show("没有找到搜索按钮"); Toaster.show("没有找到搜索按钮");
} }
@@ -448,15 +444,15 @@ public class SelectToSpeakService extends AccessibilityService {
return null; return null;
} }
private boolean stepCallDialog(Property type, String text, Step nextStep) { private boolean stepCallDialog(Property type, String text, Procedure nextProcedure) {
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
if (node != null) { if (node != null) {
Log.e(TAG, "stepCallDialog: isVisibleToUser: " + node.isVisibleToUser()); Log.e(TAG, "stepCallDialog: isVisibleToUser: " + node.isVisibleToUser());
if (node.isVisibleToUser()) { if (node.isVisibleToUser()) {
clickNode(node); clickNode(node);
Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentStep + " done"); Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
Log.e(TAG, "stepCallDialog: next: " + mCurrentStep); Log.e(TAG, "stepCallDialog: next: " + mCurrentProcedure);
return true; return true;
} else { } else {
scrollDown(); scrollDown();
@@ -466,7 +462,7 @@ public class SelectToSpeakService extends AccessibilityService {
if (mFindCount == mMaxCount) { if (mFindCount == mMaxCount) {
Log.e("stepCallDialog", "mCurrentStep: max"); Log.e("stepCallDialog", "mCurrentStep: max");
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show(); Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
mFindCount = 0; mFindCount = 0;
return false; return false;
} else { } else {
@@ -483,12 +479,12 @@ public class SelectToSpeakService extends AccessibilityService {
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
if (node != null) { if (node != null) {
clickNode(node); clickNode(node);
Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentStep + " done"); Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = Step.CLICK_QUICK_WECHAT_CALL; mCurrentProcedure = Procedure.CLICK_QUICK_WECHAT_CALL;
Log.e(TAG, "stepHome: next: " + mCurrentStep); Log.e(TAG, "stepHome: next: " + mCurrentProcedure);
return true; return true;
} else { } else {
mCurrentStep = Step.CLICK_SEARCH; mCurrentProcedure = Procedure.CLICK_SEARCH;
return false; return false;
} }
} }
@@ -497,20 +493,20 @@ public class SelectToSpeakService extends AccessibilityService {
private int mMaxCount = 5; private int mMaxCount = 5;
private boolean findContact(Property type, String text, Step nextStep) { private boolean findContact(Property type, String text, Procedure nextProcedure) {
AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text);
if (node != null) { if (node != null) {
clickNode(node); clickNode(node);
Log.e("findContact", "mCurrentStep: " + mCurrentStep + " done"); Log.e("findContact", "mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = nextStep; mCurrentProcedure = nextProcedure;
Log.e("findContact", "next: " + mCurrentStep); Log.e("findContact", "next: " + mCurrentProcedure);
mFindCount = 0; mFindCount = 0;
return true; return true;
} else { } else {
if (mFindCount == mMaxCount) { if (mFindCount == mMaxCount) {
Log.e("findContact", "mCurrentStep: max"); Log.e("findContact", "mCurrentStep: max");
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show(); Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
mFindCount = 0; mFindCount = 0;
return false; return false;
} else { } else {
@@ -620,9 +616,9 @@ public class SelectToSpeakService extends AccessibilityService {
Log.e(TAG, "stepCall: " + point); Log.e(TAG, "stepCall: " + point);
clickByPoint(point.x, point.y); clickByPoint(point.x, point.y);
// clickNode(node); // clickNode(node);
Log.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done"); Log.e(TAG, "stepCall: mCurrentStep " + mCurrentProcedure + " done");
mCurrentStep = Step.CLICK_CALL; mCurrentProcedure = Procedure.CLICK_CALL;
Log.e(TAG, "stepCall: next " + mCurrentStep); Log.e(TAG, "stepCall: next " + mCurrentProcedure);
return true; return true;
} else { } else {
Log.e(TAG, "stepCall: not found"); Log.e(TAG, "stepCall: not found");
@@ -636,7 +632,7 @@ public class SelectToSpeakService extends AccessibilityService {
if (accessibilityNodeInfo.isPresent()) { if (accessibilityNodeInfo.isPresent()) {
AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get(); AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get();
clickNode(nodeInfo); clickNode(nodeInfo);
mCurrentStep = Step.CLICK_CALL; mCurrentProcedure = Procedure.CLICK_CALL;
} else { } else {
Toaster.show("没有找到通话按钮"); Toaster.show("没有找到通话按钮");
} }
@@ -650,9 +646,9 @@ public class SelectToSpeakService extends AccessibilityService {
clickByPoint(point.x, point.y - 50); clickByPoint(point.x, point.y - 50);
clickByPoint(point.x, point.y); clickByPoint(point.x, point.y);
// clickNode(node); // clickNode(node);
Log.e(TAG, "stepAnswer: mCurrentStep " + mCurrentStep + " done"); Log.e(TAG, "stepAnswer: mCurrentStep " + mCurrentProcedure + " done");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "stepAnswer: next " + mCurrentStep); Log.e(TAG, "stepAnswer: next " + mCurrentProcedure);
return true; return true;
} else { } else {
Log.e(TAG, "stepAnswer: not found"); Log.e(TAG, "stepAnswer: not found");
@@ -667,9 +663,9 @@ public class SelectToSpeakService extends AccessibilityService {
node.getBoundsInScreen(rect); node.getBoundsInScreen(rect);
Log.e(TAG, "dialerHandsFree: rect = " + rect); Log.e(TAG, "dialerHandsFree: rect = " + rect);
clickNode(node); clickNode(node);
Log.e(TAG, "dialerHandsFree: mCurrentStep: " + mCurrentStep + " done"); Log.e(TAG, "dialerHandsFree: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "dialerHandsFree: next: " + mCurrentStep); Log.e(TAG, "dialerHandsFree: next: " + mCurrentProcedure);
return true; return true;
} else { } else {
return false; return false;
@@ -695,13 +691,13 @@ public class SelectToSpeakService extends AccessibilityService {
clickByPoint(point.x, point.y - 50); clickByPoint(point.x, point.y - 50);
clickByPoint(point.x, point.y); clickByPoint(point.x, point.y);
// clickNode(node); // clickNode(node);
Log.e(TAG, "handsFree: mCurrentStep " + mCurrentStep + " done"); Log.e(TAG, "handsFree: mCurrentStep " + mCurrentProcedure + " done");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "handsFree: next " + mCurrentStep); Log.e(TAG, "handsFree: next " + mCurrentProcedure);
return true; return true;
} else { } else {
Log.e(TAG, "handsFree: not found"); Log.e(TAG, "handsFree: not found");
mCurrentStep = Step.WAITING; mCurrentProcedure = Procedure.WAITING;
return false; return false;
} }
} }
@@ -791,7 +787,7 @@ public class SelectToSpeakService extends AccessibilityService {
return scrollScreen(0.5, 1.5); return scrollScreen(0.5, 1.5);
} }
private enum Step { private enum Procedure {
WAITING, WAITING,
//微信免提 //微信免提
WECHAT_HANDS_FREE, WECHAT_HANDS_FREE,
@@ -822,7 +818,7 @@ public class SelectToSpeakService extends AccessibilityService {
CLICK_CALL, CLICK_CALL,
CLICK_VIDEO_CALL; CLICK_VIDEO_CALL;
private Step next() { private Procedure next() {
return values()[(this.ordinal() + 1) % values().length]; return values()[(this.ordinal() + 1) % values().length];
} }
} }

View File

@@ -22,7 +22,7 @@ import com.google.gson.reflect.TypeToken;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.ttstd.elderlyassistant.R; import com.ttstd.elderlyassistant.R;
import com.ttstd.elderlyassistant.activity.setting.SettingsActivity; import com.ttstd.elderlyassistant.activity.setting.SettingsActivity;
import com.ttstd.elderlyassistant.bean.WechatContact; import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.databinding.ActivityMainBinding; import com.ttstd.elderlyassistant.databinding.ActivityMainBinding;
import com.ttstd.elderlyassistant.utils.AccessibilityUtils; import com.ttstd.elderlyassistant.utils.AccessibilityUtils;
@@ -130,10 +130,10 @@ public class MainActivity extends AppCompatActivity {
Log.e(TAG, "readNfcTag: textRecord = " + textRecord); Log.e(TAG, "readNfcTag: textRecord = " + textRecord);
Log.e(TAG, "readNfcTag: contentSize = " + contentSize); Log.e(TAG, "readNfcTag: contentSize = " + contentSize);
Gson gson = new Gson(); Gson gson = new Gson();
Type type = new TypeToken<WechatContact>() { Type type = new TypeToken<ContactInfo>() {
}.getType(); }.getType();
try { try {
WechatContact contact = gson.fromJson(textRecord, type); ContactInfo contact = gson.fromJson(textRecord, type);
Intent accessibilityIntent = new Intent(MainActivity.this, SelectToSpeakService.class); Intent accessibilityIntent = new Intent(MainActivity.this, SelectToSpeakService.class);
accessibilityIntent.putExtra("WechatInfo", contact); accessibilityIntent.putExtra("WechatInfo", contact);
startService(accessibilityIntent); startService(accessibilityIntent);

View File

@@ -22,7 +22,7 @@ import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.ttstd.elderlyassistant.R; import com.ttstd.elderlyassistant.R;
import com.ttstd.elderlyassistant.bean.WechatContact; import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.databinding.ActivityNfcBinding; import com.ttstd.elderlyassistant.databinding.ActivityNfcBinding;
import com.ttstd.elderlyassistant.gson.GsonUtils; import com.ttstd.elderlyassistant.gson.GsonUtils;
import com.ttstd.elderlyassistant.utils.NdefUtils; import com.ttstd.elderlyassistant.utils.NdefUtils;
@@ -91,11 +91,11 @@ public class NfcActivity extends AppCompatActivity {
if (!results.isEmpty()) { if (!results.isEmpty()) {
String jsonString = results.get(0); String jsonString = results.get(0);
Gson gson = new Gson(); Gson gson = new Gson();
Type type = new TypeToken<WechatContact>() { Type type = new TypeToken<ContactInfo>() {
}.getType(); }.getType();
try { try {
WechatContact contact = gson.fromJson(jsonString, type); ContactInfo contact = gson.fromJson(jsonString, type);
mBinding.setWechatContact(contact); mBinding.setContactInfo(contact);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "resolveIntent: " + e.getMessage()); Log.e(TAG, "resolveIntent: " + e.getMessage());
Toaster.showLong("NFC数据读取失败请检查配置是否正确"); Toaster.showLong("NFC数据读取失败请检查配置是否正确");
@@ -168,17 +168,17 @@ public class NfcActivity extends AppCompatActivity {
public class BtnClick { public class BtnClick {
public void write(View view) { public void write(View view) {
String name = mBinding.etName.getText().toString(); String name = mBinding.etName.getText().toString();
String phone = mBinding.etPhone.getText().toString(); String phoneNumber = mBinding.etPhone.getText().toString();
if (TextUtils.isEmpty(name)) { if (TextUtils.isEmpty(name)) {
Toaster.show("请输入联系人姓名"); Toaster.show("请输入联系人姓名");
return; return;
} }
if (TextUtils.isEmpty(phone)) { if (TextUtils.isEmpty(phoneNumber)) {
Toaster.show("请输入联系人手机号"); Toaster.show("请输入联系人手机号");
return; return;
} }
WechatContact wechatContact = new WechatContact(name, phone); ContactInfo contactInfo = new ContactInfo(name, phoneNumber);
writeTag(GsonUtils.toJSONString(wechatContact), mTag); writeTag(GsonUtils.toJSONString(contactInfo), mTag);
} }
} }
} }

View File

@@ -0,0 +1,175 @@
package com.ttstd.elderlyassistant.activity.scheme;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
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.bean.ContactInfo;
import com.ttstd.elderlyassistant.utils.AESUtil;
import java.lang.reflect.Type;
public class SchemeActivity extends AppCompatActivity {
private static final String TAG = "SchemeActivity";
private static final int REQUEST_CALL_PHONE = 1;
private static final int REQUEST_SEND_SMS = 2;
private Gson mGson = new Gson();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scheme);
Intent intent = getIntent();
parseIntent(intent);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
parseIntent(intent);
}
// 处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户授权后重新获取号码并拨号
directCall(mTempPhoneNumber);
} else {
// 权限被拒绝,提示用户
Toast.makeText(this, "权限被拒绝,无法拨号", Toast.LENGTH_SHORT).show();
}
}
// else if (requestCode == REQUEST_SEND_SMS) {
// if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// sendSms(mTempPhoneNumber); // 用户授权后发送短信
// } else {
// Toast.makeText(this, "短信发送权限被拒绝", Toast.LENGTH_SHORT).show();
// }
// }
}
private void parseIntent(Intent intent) {
if (intent == null) {
return;
}
Log.e(TAG, "parseIntent: " + intent);
Uri uri = getIntent().getData();
if (uri != null) {
String path = uri.getPath();
Log.e(TAG, "parseIntent: path = " + path);
if (TextUtils.isEmpty(path)) {
return;
}
String detail = uri.getQueryParameter("detail");
if (TextUtils.isEmpty(detail)) {
Toaster.show("没有获取到联系人数据");
return;
}
Type listType = new TypeToken<ContactInfo>() {
}.getType();
switch (path) {
case "/contactInfo":
try {
ContactInfo contactInfo = mGson.fromJson(detail, listType);
parseContactInfo(contactInfo);
} catch (Exception e) {
Log.e(TAG, "parseIntent: " + e.getMessage());
}
break;
case "/encrypted":
try {
String decryptedString = AESUtil.decrypt(detail.trim());
ContactInfo contactInfo = mGson.fromJson(decryptedString, listType);
parseContactInfo(contactInfo);
} catch (Exception e) {
Log.e(TAG, "parseEncryptedData: " + e.getMessage());
}
break;
}
}
}
private String mTempPhoneNumber;
private void parseContactInfo(ContactInfo contactInfo) {
if (contactInfo != null) {
int operation = contactInfo.getOperation();
String phoneNumber = contactInfo.getPhone();
switch (operation) {
case 1:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 未授权时请求权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CALL_PHONE);
mTempPhoneNumber = phoneNumber;
} else {
// 已授权则直接拨号
directCall(phoneNumber);
}
break;
case 2:
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
// // 动态请求权限
// ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, REQUEST_SEND_SMS);
// mTempPhoneNumber = phoneNumber;
// } else {
sendSms(phoneNumber);
// }
break;
case 3:
case 4:
Intent accessibilityIntent = new Intent(SchemeActivity.this, SelectToSpeakService.class);
accessibilityIntent.putExtra("WechatInfo", contactInfo);
startService(accessibilityIntent);
break;
case 5:
break;
case 6:
break;
default:
}
}
}
private void directCall(String phoneNumber) {
if (TextUtils.isEmpty(phoneNumber)) {
Toaster.show("未获取到手机号码");
return;
}
Intent dialerIntent = new Intent(Intent.ACTION_CALL);
Uri uri = Uri.parse("tel:" + phoneNumber);
dialerIntent.setData(uri);
startActivity(dialerIntent);
}
private void sendSms(String phoneNumber) {
if (TextUtils.isEmpty(phoneNumber)) {
Toaster.show("未获取到手机号码");
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("sms:" + phoneNumber));
intent.putExtra("sms_body", "");
startActivity(intent);
}
}

View File

@@ -2,6 +2,7 @@ package com.ttstd.elderlyassistant.activity.write;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NdefRecord; import android.nfc.NdefRecord;
import android.nfc.NfcAdapter; import android.nfc.NfcAdapter;
@@ -17,13 +18,19 @@ import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.ttstd.elderlyassistant.R; import com.ttstd.elderlyassistant.R;
import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.databinding.ActivityWriteMessageBinding; import com.ttstd.elderlyassistant.databinding.ActivityWriteMessageBinding;
import com.ttstd.elderlyassistant.gson.GsonUtils;
import com.ttstd.elderlyassistant.utils.AESUtil;
import com.ttstd.elderlyassistant.utils.HexUtils; import com.ttstd.elderlyassistant.utils.HexUtils;
import com.ttstd.elderlyassistant.utils.WriteMessageUtils; import com.ttstd.elderlyassistant.utils.WriteMessageUtils;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class WriteMessageActivity extends AppCompatActivity { public class WriteMessageActivity extends AppCompatActivity {
@@ -31,15 +38,19 @@ public class WriteMessageActivity extends AppCompatActivity {
private ActivityWriteMessageBinding mBinding; private ActivityWriteMessageBinding mBinding;
private Gson mGson = new Gson();
private NfcAdapter mNfcAdapter; private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent; private PendingIntent mPendingIntent;
private Tag mTag; private Tag mTag;
private int mOperationId;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_write_message); // setContentView(R.layout.activity_write_message);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_write_message); mBinding = DataBindingUtil.setContentView(this, R.layout.activity_write_message);
mBinding.setLifecycleOwner(this);
mBinding.setClick(new BtnClick()); mBinding.setClick(new BtnClick());
// 获取NFC适配器实例 // 获取NFC适配器实例
@@ -124,10 +135,48 @@ public class WriteMessageActivity extends AppCompatActivity {
// 提取第一条记录的文本 // 提取第一条记录的文本
NdefRecord record = ndefMessage.getRecords()[0]; NdefRecord record = ndefMessage.getRecords()[0];
String payload = new String(record.getPayload(), StandardCharsets.UTF_8); String payload = new String(record.getPayload(), StandardCharsets.UTF_8);
Log.e("NFC", "NDEF Text: " + payload.substring(1)); // 跳过编码头 String text = payload.substring(1);
Log.e(TAG, "parseNdefData: NDEF Text: " + text); // 跳过编码头
Uri uri = Uri.parse(text);
String path = uri.getPath();
if (TextUtils.isEmpty(path)) {
Toaster.show("没有获取到联系人数据");
return;
}
String detail = uri.getQueryParameter("detail");
if (TextUtils.isEmpty(detail)) {
Toaster.show("没有获取到联系人数据");
return;
}
Type listType = new TypeToken<ContactInfo>() {
}.getType();
switch (path) {
case "/contactInfo":
mBinding.rbNoEncryption.setChecked(true);
try {
ContactInfo contactInfo = mGson.fromJson(detail, listType);
setContactInfo(contactInfo);
} catch (Exception e) {
Log.e(TAG, "parseNdefData: " + e.getMessage());
}
break;
case "/encrypted":
mBinding.rbEncryption.setChecked(true);
try {
String trimString = detail.trim();
String decryptedString = AESUtil.decrypt(trimString);
ContactInfo contactInfo = mGson.fromJson(decryptedString, listType);
setContactInfo(contactInfo);
} catch (Exception e) {
Log.e(TAG, "parseEncryptedData: " + e.getMessage());
}
break;
}
} }
} catch (Exception e) { } catch (Exception e) {
Log.e("NFC", "NDEF解析失败" + e.getMessage()); Log.e(TAG, "NDEF解析失败" + e.getMessage());
} finally { } finally {
try { try {
ndef.close(); ndef.close();
@@ -138,21 +187,70 @@ public class WriteMessageActivity extends AppCompatActivity {
} }
} }
private void setContactInfo(ContactInfo contactInfo) {
mBinding.setContactInfo(contactInfo);
}
public class BtnClick { public class BtnClick {
public void write(View view) { public void write(View view) {
if (mTag == null) { if (mTag == null) {
Toaster.showLong("NFC卡片未连接"); Toaster.showLong("NFC卡片未连接");
return; return;
} }
Editable editable = mBinding.etPhone.getText(); Editable nameEditable = mBinding.etName.getText();
if (TextUtils.isEmpty(editable)) { Editable phoneEditable = mBinding.etPhone.getText();
Toaster.showLong("请输入电话"); Editable wxidEditable = mBinding.etWxid.getText();
Editable qqEditable = mBinding.etQq.getText();
Editable tagEditable = mBinding.etTag.getText();
if (TextUtils.isEmpty(nameEditable)) {
Toaster.showLong("请输入姓名或昵称");
return; return;
} }
String phone = editable.toString(); String name = nameEditable.toString();
if (!phone.startsWith("+86")) String phoneNumber = phoneEditable.toString();
phone = "+86" + phone; String wechatId = wxidEditable.toString();
WriteMessageUtils.writePhoneToTag(mTag, phone); String qq = qqEditable.toString();
String tag = tagEditable.toString();
int checkedId = mBinding.rgOperation.getCheckedRadioButtonId();
if (checkedId != -1) {
View checkedView = mBinding.rgOperation.findViewById(checkedId);
int index = mBinding.rgOperation.indexOfChild(checkedView);
Log.d("SelectedIndex", "位置: " + index); // 输出索引从0开始
mOperationId = index + 1;
} else {
Log.d("SelectedIndex", "未选中任何项");
mOperationId = 1;
}
ContactInfo contactInfo = new ContactInfo();
contactInfo.setName(name);
contactInfo.setPhone(phoneNumber);
contactInfo.setWxid(wechatId);
contactInfo.setQq(qq);
contactInfo.setTag(tag);
contactInfo.setOperation(mOperationId);
if (mBinding.rbEncryption.isChecked()) {
try {
String jsonString = GsonUtils.toJSONString(contactInfo);
String encryptedString = AESUtil.encrypt(jsonString.trim());
WriteMessageUtils.writeUrlAddPackage(mTag, "elderlyassistant://ttstd.com/encrypted?detail=" + encryptedString, "com.ttstd.elderlyassistant");
Toaster.show("写入联系人成功");
} catch (Exception e) {
e.printStackTrace();
Toaster.show("写入联系人失败");
}
} else {
try {
WriteMessageUtils.writeUrlAddPackage(mTag, "elderlyassistant://ttstd.com/contactInfo?detail=" + GsonUtils.toJSONString(contactInfo), "com.ttstd.elderlyassistant");
Toaster.show("写入联系人成功");
} catch (Exception e) {
e.printStackTrace();
Toaster.show("写入联系人失败");
}
}
} }
public void writeQqMusic(View view) { public void writeQqMusic(View view) {
@@ -160,15 +258,29 @@ public class WriteMessageActivity extends AppCompatActivity {
Toaster.showLong("NFC卡片未连接"); Toaster.showLong("NFC卡片未连接");
return; return;
} }
WriteMessageUtils.writeUrlToTag(mTag, "qqmusic://qq.com/media/playSonglist?p={\"song\":[{\"songmid\":\"003Hr2m138zmPR\"}]}"); try {
WriteMessageUtils.writeUrlToTag(mTag, "qqmusic://qq.com/media/playSonglist?p={\"song\":[{\"songmid\":\"003Hr2m138zmPR\"}]}");
Toaster.show("写入联系人成功");
} catch (Exception e) {
e.printStackTrace();
Toaster.show("写入联系人失败");
}
} }
public void writeQqMusicForIos(View view) { public void writeUrlAddPackage(View view) {
if (mTag == null) { if (mTag == null) {
Toaster.showLong("NFC卡片未连接"); Toaster.showLong("NFC卡片未连接");
return; return;
} }
WriteMessageUtils.writeUrlToTagIos(mTag, "qqmusic://qq.com/media/playSonglist?p={\"song\":[{\"songmid\":\"003aAPj81VWrbL\"}]}","com.tencent.qqmusic"); try {
WriteMessageUtils.writeUrlAddPackage(mTag,
"elderlyassistant://ttstd.com/contactInfo?detail={ \"name\": \"教父\", \"phone\": \"18108347897\", \"tag\": \"家人\", \"wxid\": \"wx_godfather\", \"operation\": 1, \"version\": 1 }",
"com.ttstd.elderlyassistant");
Toaster.show("写入联系人成功");
} catch (Exception e) {
e.printStackTrace();
Toaster.show("写入联系人失败");
}
} }
} }

View File

@@ -0,0 +1,11 @@
package com.ttstd.elderlyassistant.activity.write;
import androidx.databinding.ObservableInt;
import androidx.lifecycle.ViewModel;
import com.ttstd.elderlyassistant.R;
public class WriteMessageViewModel extends ViewModel {
}

View File

@@ -7,24 +7,26 @@ import com.google.gson.JsonParser;
import java.io.Serializable; import java.io.Serializable;
public class WechatContact implements Serializable { public class ContactInfo implements Serializable {
private static final long serialVersionUID = -5570426400839799887L; private static final long serialVersionUID = -5570426400839799887L;
public String name; public String name;
public String phone; public String phone;
public String tag; public String tag;
public String wxid;
public String qq;
/** /**
* 0 电话 1 短信 2 微信语音 3 微信视频 * 1 电话 2 短信 3 微信语音 4 微信视频 5 QQ语音 6 QQ视频
*/ */
int operation; int operation;
int version; int version;
public WechatContact() { public ContactInfo() {
this.version = 1;
} }
public WechatContact(String name, String phone) { public ContactInfo(String name, String phone) {
this.name = name; this.name = name;
this.phone = phone; this.phone = phone;
this.operation = 3; this.operation = 3;
@@ -47,7 +49,6 @@ public class WechatContact implements Serializable {
this.phone = phone; this.phone = phone;
} }
public String getTag() { public String getTag() {
return tag; return tag;
} }
@@ -56,6 +57,22 @@ public class WechatContact implements Serializable {
this.tag = tag; this.tag = tag;
} }
public String getWxid() {
return wxid;
}
public void setWxid(String wxid) {
this.wxid = wxid;
}
public String getQq() {
return qq;
}
public void setQq(String qq) {
this.qq = qq;
}
public int getOperation() { public int getOperation() {
return operation; return operation;
} }
@@ -64,6 +81,14 @@ public class WechatContact implements Serializable {
this.operation = operation; this.operation = operation;
} }
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {

View File

@@ -0,0 +1,35 @@
package com.ttstd.elderlyassistant.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AESUtil {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; // 使用 CBC 模式和 PKCS5 填充
private static final String SECRET_KEY = "YourSecretKey123"; // 密钥16/24/32 字节)
private static final String IV = "RandomInitVector"; // 初始化向量16 字节)
// 加密
public static String encrypt(String input) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encryptedBytes = cipher.doFinal(input.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes); // Base64 编码输出
}
// 解密
public static String decrypt(String encryptedInput) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decodedBytes = Base64.getDecoder().decode(encryptedInput);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
}

View File

@@ -15,60 +15,45 @@ import java.io.IOException;
public class WriteMessageUtils { public class WriteMessageUtils {
private static final String TAG = "WriteMessageUtils"; private static final String TAG = "WriteMessageUtils";
public static void writePhoneToTag(Tag tag, String phoneNumber) { public static void writePhoneToTag(Tag tag, String phoneNumber) throws Exception {
// 构建tel:协议的URI // 构建tel:协议的URI
Uri uri = Uri.parse("tel:" + phoneNumber); Uri uri = Uri.parse("tel:" + phoneNumber);
// 创建URI类型的NdefRecord // 创建URI类型的NdefRecord
NdefRecord uriRecord = NdefRecord.createUri(uri); NdefRecord uriRecord = NdefRecord.createUri(uri);
NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord}); NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord});
try { Ndef ndef = Ndef.get(tag);
Ndef ndef = Ndef.get(tag); ndef.connect();
ndef.connect(); ndef.writeNdefMessage(message); // 写入NDEF消息
ndef.writeNdefMessage(message); // 写入NDEF消息 ndef.close();
ndef.close();
Toaster.show("写入联系人成功");
} catch (IOException | FormatException e) {
e.printStackTrace();
}
} }
public static void writeUrlToTag(Tag tag, String url) { public static void writeUrlToTag(Tag tag, String url) throws Exception {
// 确保URL包含协议如http:// // 确保URL包含协议如http://
Uri uri = Uri.parse(url); Uri uri = Uri.parse(url);
NdefRecord uriRecord = NdefRecord.createUri(uri); NdefRecord uriRecord = NdefRecord.createUri(uri);
NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord}); NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord});
try { Ndef ndef = Ndef.get(tag);
Ndef ndef = Ndef.get(tag); ndef.connect();
ndef.connect(); ndef.writeNdefMessage(message);
ndef.writeNdefMessage(message); ndef.close();
ndef.close();
Toaster.show("写入Url成功");
} catch (IOException | FormatException e) {
e.printStackTrace();
}
} }
public static void writeUrlToTagIos(Tag tag, String url, String pkg) { public static void writeUrlAddPackage(Tag tag, String url, String pkg) throws Exception {
Log.e(TAG, "writeUrlAddPackage: url = " + url);
String fixedUrl = url.replace("+", "%2B"); // 替换 + 为 %2B
// 1. 创建HTTP RecordHTTPS链接 // 1. 创建HTTP RecordHTTPS链接
Uri uri = Uri.parse(url); Uri uri = Uri.parse(fixedUrl);
NdefRecord httpRecord = NdefRecord.createUri(uri); NdefRecord httpRecord = NdefRecord.createUri(uri);
// 2. 创建Android应用包名Record
// 2. 创建Android应用包名Record
NdefRecord androidRecord = NdefRecord.createApplicationRecord(pkg); NdefRecord androidRecord = NdefRecord.createApplicationRecord(pkg);
// 3. 按顺序组合记录
// 3. 按顺序组合记录
NdefMessage message = new NdefMessage(new NdefRecord[]{httpRecord, androidRecord}); NdefMessage message = new NdefMessage(new NdefRecord[]{httpRecord, androidRecord});
// 4. 写入NFC标签
// 4. 写入NFC标签 Ndef ndef = Ndef.get(tag); // 获取标签对象
try { ndef.connect();
Ndef ndef = Ndef.get(tag); // 获取标签对象 ndef.writeNdefMessage(message);
ndef.connect();
ndef.writeNdefMessage(message);
} catch (Exception e) {
Log.e(TAG, "writeUrlToTagIos: " + e.getMessage());
}
} }
} }

View File

@@ -11,8 +11,8 @@
type="com.ttstd.elderlyassistant.activity.nfc.NfcActivity.BtnClick" /> type="com.ttstd.elderlyassistant.activity.nfc.NfcActivity.BtnClick" />
<variable <variable
name="wechatContact" name="contactInfo"
type="com.ttstd.elderlyassistant.bean.WechatContact" /> type="com.ttstd.elderlyassistant.bean.ContactInfo" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@@ -24,10 +24,10 @@
android:id="@+id/et_name" android:id="@+id/et_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ems="10" android:maxLength="15"
android:hint="Name" android:hint="Name"
android:inputType="text" android:inputType="text"
android:text="@{wechatContact.name}" android:text="@{contactInfo.name}"
android:maxLines="1" android:maxLines="1"
android:singleLine="true" android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -38,12 +38,12 @@
android:id="@+id/et_phone" android:id="@+id/et_phone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ems="10" android:maxLength="15"
android:hint="phone" android:hint="phoneNumber"
android:inputType="phone" android:inputType="phone"
android:maxLines="1" android:maxLines="1"
android:singleLine="true" android:singleLine="true"
android:text="@{wechatContact.phone}" android:text="@{contactInfo.phone}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_name" /> app:layout_constraintTop_toBottomOf="@id/et_name" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.scheme.SchemeActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -9,67 +9,235 @@
<variable <variable
name="click" name="click"
type="com.ttstd.elderlyassistant.activity.write.WriteMessageActivity.BtnClick" /> type="com.ttstd.elderlyassistant.activity.write.WriteMessageActivity.BtnClick" />
<variable
name="contactInfo"
type="com.ttstd.elderlyassistant.bean.ContactInfo" />
<variable
name="viewModel"
type="com.ttstd.elderlyassistant.activity.write.WriteMessageViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <androidx.core.widget.NestedScrollView
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
<EditText
android:id="@+id/et_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="phone"
android:inputType="phone"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::write}"
android:text="写入"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_phone" />
<TextView
android:id="@+id/tv_id"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="姓名、微信昵称(必填)"
android:maxLines="1"
android:singleLine="true"
android:text="@{contactInfo.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="手机号码(选填)"
android:inputType="phone"
android:maxLength="15"
android:maxLines="1"
android:singleLine="true"
android:text="@{contactInfo.phone}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_wxid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="微信ID或QQ号码(选填)"
android:maxLines="1"
android:singleLine="true"
android:text="@{contactInfo.wxid}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_qq"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="QQ号码"
android:maxLines="1"
android:singleLine="true"
android:text="@{contactInfo.qq}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="微信通讯录分组标签(选填)"
android:maxLines="1"
android:text="@{contactInfo.tag}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="自动操作类型:"
android:textSize="18sp" />
<RadioGroup
android:id="@+id/rg_operation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkedButton="@id/rb_dialere"
android:orientation="vertical">
<RadioButton
android:id="@+id/rb_dialere"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==1}"
android:maxLines="1"
android:singleLine="true"
android:text="拨打电话号码" />
<RadioButton
android:id="@+id/rb_sms"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==2}"
android:maxLines="1"
android:singleLine="true"
android:text="发送短信" />
<RadioButton
android:id="@+id/rb_wechat_voice"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==3}"
android:maxLines="1"
android:singleLine="true"
android:text="拨打微信电话" />
<Button <RadioButton
android:layout_width="wrap_content" android:id="@+id/rb_wechat_video"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:onClick="@{click::writeQqMusic}" android:layout_height="40dp"
android:text="QQ音乐" android:checked="@{contactInfo.operation==4}"
app:layout_constraintEnd_toEndOf="parent" android:maxLines="1"
app:layout_constraintStart_toStartOf="parent" android:singleLine="true"
app:layout_constraintTop_toBottomOf="@+id/et_phone" /> android:text="拨打微信视频" />
<Button <RadioButton
android:layout_width="wrap_content" android:id="@+id/rb_qq_voice"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:onClick="@{click::writeQqMusicForIos}" android:layout_height="40dp"
android:text="QQ音乐兼容iPhone" android:checked="@{contactInfo.operation==5}"
app:layout_constraintEnd_toEndOf="parent" android:maxLines="1"
app:layout_constraintStart_toStartOf="parent" android:singleLine="true"
app:layout_constraintTop_toBottomOf="@+id/et_phone" /> android:text="拨打QQ电话" />
</LinearLayout>
<RadioButton
android:id="@+id/rb_qq_video"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==6}"
android:maxLines="1"
android:singleLine="true"
android:text="拨打QQ视频" />
</RadioGroup>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="是否进行联系人加密:"
android:textSize="18sp" />
<RadioGroup
android:id="@+id/rg_encrypt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkedButton="@id/rb_no_encryption"
android:orientation="vertical">
<RadioButton
android:id="@+id/rb_no_encryption"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_weight="1"
android:maxLines="1"
android:singleLine="true"
android:text="不加密(若要分享给他人请选择不加密)" />
<RadioButton
android:id="@+id/rb_encryption"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_weight="1"
android:maxLines="1"
android:singleLine="true"
android:text="加密(若担心个人信息泄露请选择加密,或只填写昵称)" />
</RadioGroup>
<TextView
android:id="@+id/tv_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:onClick="@{click::write}"
android:text="写入"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_phone" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::writeQqMusic}"
android:text="QQ音乐"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_phone" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::writeUrlAddPackage}"
android:text="Scheme+app"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_phone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>