优化通过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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ttstd.elderlyassistant">
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- NFC权限 -->
<uses-permission android:name="android.permission.NFC" /> <!-- 声明只有带NFC的手机才可以下载 -->
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
@@ -53,16 +57,31 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- <activity-->
<!-- android:name=".activity.third_party.NfcQQMusicActivity"-->
<!-- android:launchMode="singleTask">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.nfc.action.NDEF_DISCOVERED" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- &lt;!&ndash; 匹配 QQ 音乐 Scheme &ndash;&gt;-->
<!-- <data android:scheme="qqmusic" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<!-- <activity -->
<!-- android:name=".activity.third_party.NfcQQMusicActivity" -->
<!-- android:launchMode="singleTask"> -->
<!-- <intent-filter> -->
<!-- <action android:name="android.nfc.action.NDEF_DISCOVERED" /> -->
<!-- <category android:name="android.intent.category.DEFAULT" /> -->
<!-- &lt;!&ndash; 匹配 QQ 音乐 Scheme &ndash;&gt; -->
<!-- <data android:scheme="qqmusic" /> -->
<!-- </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
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.tencent.mmkv.MMKV;
import com.ttstd.elderlyassistant.bean.WechatContact;
import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.config.CommonConfig;
import java.util.List;
@@ -39,7 +39,7 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
/**
* 通过微信标签最高支持8.0.498.0.50 获取不到数据
* 通过微信标签最高支持8.0.49
* 8.0.54 可以获取
* 通过 {@link AccessibilityService#getWindows}和修改accessibility-service 配置能遍历屏幕元素
*/
@@ -56,8 +56,8 @@ public class SelectToSpeakService extends AccessibilityService {
private static final String TAG_TEXT = "标签";
private static final String PARENT_VIDEO_TEXT = "视频通话";
private static final String VOICE_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 = "扬声器已关";
@@ -65,17 +65,19 @@ public class SelectToSpeakService extends AccessibilityService {
private static final String DIALER_HANDS_FREE_TEXT = "免提";
private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭";
public static final int TYPE_VOICE = 2;
public static final int TYPE_VIDEO = 3;
public static final int TYPE_VOICE = 3;
public static final int TYPE_VIDEO = 4;
private static final int WAIT_TIME = 1500;
private WechatContact mContact;
private String mName = "";//微信昵称
private ContactInfo mContact;
/**
* 微信昵称
*/
private String mName = "";
private int mCallType = TYPE_VOICE;
private Step mCurrentStep = Step.WAITING;
private Procedure mCurrentProcedure = Procedure.WAITING;
private boolean mAutoAccept = false;
private Disposable mDisposable;
@@ -92,11 +94,11 @@ public class SelectToSpeakService extends AccessibilityService {
Log.e(TAG, "onCreate: ");
registerSettingReceiver();
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
dealingAccessibilityEvent();
initDisposable();
}
private void dealingAccessibilityEvent() {
private void initDisposable() {
mDisposable = Observable.create(new ObservableOnSubscribe<AccessibilityEvent>() {
@Override
public void subscribe(@NonNull ObservableEmitter<AccessibilityEvent> emitter) throws Throwable {
@@ -106,23 +108,23 @@ public class SelectToSpeakService extends AccessibilityService {
.subscribe(new Consumer<AccessibilityEvent>() {
@Override
public void accept(AccessibilityEvent accessibilityEvent) throws Throwable {
Log.e(TAG, "accept: ");
_onAccessibilityEvent(accessibilityEvent);
Log.e(TAG, "initDisposable accept: ");
parseAccessibilityEvent(accessibilityEvent);
}
});
Log.e(TAG, "dealingAccessibilityEvent: mDisposable hash = " + mDisposable.hashCode());
Log.e(TAG, "initDisposable: mDisposable hash = " + mDisposable.hashCode());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand: ");
if (intent != null) {
mContact = (WechatContact) intent.getSerializableExtra("WechatInfo");
mContact = (ContactInfo) intent.getSerializableExtra("WechatInfo");
Log.e(TAG, "onStartCommand: wechatInfo = " + mContact);
if (mContact != null) {
mName = mContact.getName();
mCallType = mContact.getOperation();
mCurrentStep = Step.CLICK_HOME;
mCurrentProcedure = Procedure.CLICK_HOME;
launchWeChat();
} else {
Toaster.showLong("没有获取到联系人数据");
@@ -155,12 +157,12 @@ public class SelectToSpeakService extends AccessibilityService {
}
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) {
String currentPackageName = event.getPackageName().toString();
String currentClassName = event.getClassName().toString();
switch (mCurrentStep) {
switch (mCurrentProcedure) {
case WAITING:
if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) {
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.LoginPasswordUI":
Toaster.showLong("请先登录微信");
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
break;
case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI":
@@ -197,20 +199,20 @@ public class SelectToSpeakService extends AccessibilityService {
*
* @param event
*/
private void _onAccessibilityEvent(AccessibilityEvent event) {
Log.e(TAG, "_onAccessibilityEvent: " + mCurrentStep);
switch (mCurrentStep) {
private void parseAccessibilityEvent(AccessibilityEvent event) {
Log.e(TAG, "parseAccessibilityEvent: " + mCurrentProcedure);
switch (mCurrentProcedure) {
case WAITING:
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_CALL_AUTO_ACCEPT_KEY, false);
Log.e(TAG, "_onAccessibilityEvent: mAutoAccept = " + mAutoAccept);
Log.e(TAG, "parseAccessibilityEvent: mAutoAccept = " + mAutoAccept);
if (!mAutoAccept) {
return;
}
if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) {
mCurrentStep = Step.WECHAT_HANDS_FREE;
mCurrentProcedure = Procedure.WECHAT_HANDS_FREE;
Toast.makeText(this, "已自动接听视频/语音", Toast.LENGTH_LONG).show();
} else {
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
// clickAnswer();
}
break;
@@ -221,29 +223,29 @@ public class SelectToSpeakService extends AccessibilityService {
if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) {
dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT);
} else {
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
}
break;
case CLICK_HOME://主页能找到直接点击进去更多
if (stepHome(Property.TEXT, mName)) {
Log.e(TAG, "_onAccessibilityEvent: not found contact in home");
Log.e(TAG, "parseAccessibilityEvent: not found contact in home");
} 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);
}
break;
case CLICK_SEARCH:
putString(mName, Step.CLICK_SEARCH_CONTACT);
putString(mName, Procedure.CLICK_SEARCH_CONTACT);
break;
case CLICK_SEARCH_CONTACT:
if (findSearchContact(Step.FIND_CONTACT)) {
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
if (findSearchContact(Procedure.FIND_CONTACT)) {
findSearchContact(Property.TEXT, mName, Procedure.CLICK_QUICK_WECHAT_CALL);
} else {
Toaster.show("没有找到联系人");
}
break;
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);
break;
case CLICK_TARGET://点击视频通话
@@ -252,40 +254,34 @@ public class SelectToSpeakService extends AccessibilityService {
break;
case CLICK_CONTACT://进入通讯录界面
if (stepHome(Property.TEXT, CONTACT_TEXT, Step.FIND_TAG)) {
Log.e(TAG, "_onAccessibilityEvent: enter contact");
if (stepHome(Property.TEXT, CONTACT_TEXT, Procedure.FIND_TAG)) {
Log.e(TAG, "parseAccessibilityEvent: enter contact");
} else {
touchContact();
}
break;
case FIND_CONTACT://模拟滑动找到联系人
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
findSearchContact(Property.TEXT, mName, Procedure.CLICK_QUICK_WECHAT_CALL);
break;
case FIND_TAG:
step(Property.TEXT, TAG_TEXT, Step.CLICK_TAG);
step(Property.TEXT, TAG_TEXT, Procedure.CLICK_TAG);
break;
case CLICK_TAG:
break;
case CLICK_NAME://点击item
findContact(Property.TEXT, mName, Step.CLICK_INFO);
findContact(Property.TEXT, mName, Procedure.CLICK_INFO);
break;
case CLICK_INFO://进入个人信息页面
stepCallDialog(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL);
stepCallDialog(Property.TEXT, DIALER_TEXT, Procedure.CLICK_CALL);
break;
case CLICK_CALL://打视频或者电话
if (mCallType == TYPE_VIDEO) {
step(Property.TEXT, VIDEO_TEXT, Step.WAITING);
step(Property.TEXT, VIDEO_TEXT, Procedure.WAITING);
} else if (mCallType == TYPE_VOICE) {
step(Property.TEXT, CALL_TEXT, Step.WAITING);
step(Property.TEXT, VOICE_TEXT, Procedure.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:
}
}
@@ -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);
if (node != null) {
Rect rect = new Rect();
@@ -330,9 +326,9 @@ public class SelectToSpeakService extends AccessibilityService {
return false;
}
clickNode(node);
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done");
mCurrentStep = nextStep;
Log.e(TAG, "step: next: " + mCurrentStep);
Log.e(TAG, "step: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = nextProcedure;
Log.e(TAG, "step: next: " + mCurrentProcedure);
return true;
} else {
return false;
@@ -340,7 +336,7 @@ public class SelectToSpeakService extends AccessibilityService {
}
// 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);
if (node != null) {
Rect rect = new Rect();
@@ -350,9 +346,9 @@ public class SelectToSpeakService extends AccessibilityService {
return false;
}
clickNode(node);
Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done");
mCurrentStep = nextStep;
Log.e(TAG, "step: next: " + mCurrentStep);
Log.e(TAG, "step: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = nextProcedure;
Log.e(TAG, "step: next: " + mCurrentProcedure);
return true;
} else {
return false;
@@ -363,35 +359,35 @@ public class SelectToSpeakService extends AccessibilityService {
private void touchContact() {
boolean successful = clickByPoint(268, 1440);
if (successful) {
mCurrentStep = Step.FIND_TAG;
mCurrentProcedure = Procedure.FIND_TAG;
} else {
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
Toaster.show("点击失败,请重试");
}
}
private boolean findSearchContact(Step nextStep) {
private boolean findSearchContact(Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf");
Log.e(TAG, "findSearchContact: " + nodeInfos);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
return optional.isPresent();
}
private void findSearchContact(Property type, String text, Step nextStep) {
private void findSearchContact(Property type, String text, Procedure nextProcedure) {
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/odf");
Log.e(TAG, "findSearchContact: " + nodeInfos);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
if (optional.isPresent()) {
AccessibilityNodeInfo nodeInfo = optional.get();
clickNode(nodeInfo);
mCurrentStep = nextStep;
mCurrentProcedure = nextProcedure;
} else {
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");
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
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
nodeInfo.performAction(ACTION_IME_ENTER_ID);
}
mCurrentStep = nextStep;
mCurrentProcedure = nextProcedure;
} else {
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);
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
if (optional.isPresent()) {
AccessibilityNodeInfo nodeInfo = optional.get();
clickNode(nodeInfo);
mCurrentStep = nextStep;
mCurrentProcedure = nextProcedure;
} else {
Toaster.show("没有找到搜索按钮");
}
@@ -448,15 +444,15 @@ public class SelectToSpeakService extends AccessibilityService {
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);
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);
Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = nextProcedure;
Log.e(TAG, "stepCallDialog: next: " + mCurrentProcedure);
return true;
} else {
scrollDown();
@@ -466,7 +462,7 @@ public class SelectToSpeakService extends AccessibilityService {
if (mFindCount == mMaxCount) {
Log.e("stepCallDialog", "mCurrentStep: max");
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
mFindCount = 0;
return false;
} else {
@@ -483,12 +479,12 @@ public class SelectToSpeakService extends AccessibilityService {
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);
Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = Procedure.CLICK_QUICK_WECHAT_CALL;
Log.e(TAG, "stepHome: next: " + mCurrentProcedure);
return true;
} else {
mCurrentStep = Step.CLICK_SEARCH;
mCurrentProcedure = Procedure.CLICK_SEARCH;
return false;
}
}
@@ -497,20 +493,20 @@ public class SelectToSpeakService extends AccessibilityService {
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);
if (node != null) {
clickNode(node);
Log.e("findContact", "mCurrentStep: " + mCurrentStep + " done");
mCurrentStep = nextStep;
Log.e("findContact", "next: " + mCurrentStep);
Log.e("findContact", "mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = nextProcedure;
Log.e("findContact", "next: " + mCurrentProcedure);
mFindCount = 0;
return true;
} else {
if (mFindCount == mMaxCount) {
Log.e("findContact", "mCurrentStep: max");
Toast.makeText(this, "没有找到联系人", Toast.LENGTH_LONG).show();
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
mFindCount = 0;
return false;
} else {
@@ -620,9 +616,9 @@ public class SelectToSpeakService extends AccessibilityService {
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);
Log.e(TAG, "stepCall: mCurrentStep " + mCurrentProcedure + " done");
mCurrentProcedure = Procedure.CLICK_CALL;
Log.e(TAG, "stepCall: next " + mCurrentProcedure);
return true;
} else {
Log.e(TAG, "stepCall: not found");
@@ -636,7 +632,7 @@ public class SelectToSpeakService extends AccessibilityService {
if (accessibilityNodeInfo.isPresent()) {
AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get();
clickNode(nodeInfo);
mCurrentStep = Step.CLICK_CALL;
mCurrentProcedure = Procedure.CLICK_CALL;
} else {
Toaster.show("没有找到通话按钮");
}
@@ -650,9 +646,9 @@ public class SelectToSpeakService extends AccessibilityService {
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);
Log.e(TAG, "stepAnswer: mCurrentStep " + mCurrentProcedure + " done");
mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "stepAnswer: next " + mCurrentProcedure);
return true;
} else {
Log.e(TAG, "stepAnswer: not found");
@@ -667,9 +663,9 @@ public class SelectToSpeakService extends AccessibilityService {
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);
Log.e(TAG, "dialerHandsFree: mCurrentStep: " + mCurrentProcedure + " done");
mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "dialerHandsFree: next: " + mCurrentProcedure);
return true;
} else {
return false;
@@ -695,13 +691,13 @@ public class SelectToSpeakService extends AccessibilityService {
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);
Log.e(TAG, "handsFree: mCurrentStep " + mCurrentProcedure + " done");
mCurrentProcedure = Procedure.WAITING;
Log.e(TAG, "handsFree: next " + mCurrentProcedure);
return true;
} else {
Log.e(TAG, "handsFree: not found");
mCurrentStep = Step.WAITING;
mCurrentProcedure = Procedure.WAITING;
return false;
}
}
@@ -791,7 +787,7 @@ public class SelectToSpeakService extends AccessibilityService {
return scrollScreen(0.5, 1.5);
}
private enum Step {
private enum Procedure {
WAITING,
//微信免提
WECHAT_HANDS_FREE,
@@ -822,7 +818,7 @@ public class SelectToSpeakService extends AccessibilityService {
CLICK_CALL,
CLICK_VIDEO_CALL;
private Step next() {
private Procedure next() {
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.ttstd.elderlyassistant.R;
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.utils.AccessibilityUtils;
@@ -130,10 +130,10 @@ public class MainActivity extends AppCompatActivity {
Log.e(TAG, "readNfcTag: textRecord = " + textRecord);
Log.e(TAG, "readNfcTag: contentSize = " + contentSize);
Gson gson = new Gson();
Type type = new TypeToken<WechatContact>() {
Type type = new TypeToken<ContactInfo>() {
}.getType();
try {
WechatContact contact = gson.fromJson(textRecord, type);
ContactInfo contact = gson.fromJson(textRecord, type);
Intent accessibilityIntent = new Intent(MainActivity.this, SelectToSpeakService.class);
accessibilityIntent.putExtra("WechatInfo", contact);
startService(accessibilityIntent);

View File

@@ -22,7 +22,7 @@ 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.WechatContact;
import com.ttstd.elderlyassistant.bean.ContactInfo;
import com.ttstd.elderlyassistant.databinding.ActivityNfcBinding;
import com.ttstd.elderlyassistant.gson.GsonUtils;
import com.ttstd.elderlyassistant.utils.NdefUtils;
@@ -91,11 +91,11 @@ public class NfcActivity extends AppCompatActivity {
if (!results.isEmpty()) {
String jsonString = results.get(0);
Gson gson = new Gson();
Type type = new TypeToken<WechatContact>() {
Type type = new TypeToken<ContactInfo>() {
}.getType();
try {
WechatContact contact = gson.fromJson(jsonString, type);
mBinding.setWechatContact(contact);
ContactInfo contact = gson.fromJson(jsonString, type);
mBinding.setContactInfo(contact);
} catch (Exception e) {
Log.e(TAG, "resolveIntent: " + e.getMessage());
Toaster.showLong("NFC数据读取失败请检查配置是否正确");
@@ -168,17 +168,17 @@ public class NfcActivity extends AppCompatActivity {
public class BtnClick {
public void write(View view) {
String name = mBinding.etName.getText().toString();
String phone = mBinding.etPhone.getText().toString();
String phoneNumber = mBinding.etPhone.getText().toString();
if (TextUtils.isEmpty(name)) {
Toaster.show("请输入联系人姓名");
return;
}
if (TextUtils.isEmpty(phone)) {
if (TextUtils.isEmpty(phoneNumber)) {
Toaster.show("请输入联系人手机号");
return;
}
WechatContact wechatContact = new WechatContact(name, phone);
writeTag(GsonUtils.toJSONString(wechatContact), mTag);
ContactInfo contactInfo = new ContactInfo(name, phoneNumber);
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.content.Intent;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
@@ -17,13 +18,19 @@ 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.ContactInfo;
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.WriteMessageUtils;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
public class WriteMessageActivity extends AppCompatActivity {
@@ -31,15 +38,19 @@ public class WriteMessageActivity extends AppCompatActivity {
private ActivityWriteMessageBinding mBinding;
private Gson mGson = new Gson();
private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private Tag mTag;
private int mOperationId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_write_message);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_write_message);
mBinding.setLifecycleOwner(this);
mBinding.setClick(new BtnClick());
// 获取NFC适配器实例
@@ -124,10 +135,48 @@ public class WriteMessageActivity extends AppCompatActivity {
// 提取第一条记录的文本
NdefRecord record = ndefMessage.getRecords()[0];
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) {
Log.e("NFC", "NDEF解析失败" + e.getMessage());
Log.e(TAG, "NDEF解析失败" + e.getMessage());
} finally {
try {
ndef.close();
@@ -138,21 +187,70 @@ public class WriteMessageActivity extends AppCompatActivity {
}
}
private void setContactInfo(ContactInfo contactInfo) {
mBinding.setContactInfo(contactInfo);
}
public class BtnClick {
public void write(View view) {
if (mTag == null) {
Toaster.showLong("NFC卡片未连接");
return;
}
Editable editable = mBinding.etPhone.getText();
if (TextUtils.isEmpty(editable)) {
Toaster.showLong("请输入电话");
Editable nameEditable = mBinding.etName.getText();
Editable phoneEditable = mBinding.etPhone.getText();
Editable wxidEditable = mBinding.etWxid.getText();
Editable qqEditable = mBinding.etQq.getText();
Editable tagEditable = mBinding.etTag.getText();
if (TextUtils.isEmpty(nameEditable)) {
Toaster.showLong("请输入姓名或昵称");
return;
}
String phone = editable.toString();
if (!phone.startsWith("+86"))
phone = "+86" + phone;
WriteMessageUtils.writePhoneToTag(mTag, phone);
String name = nameEditable.toString();
String phoneNumber = phoneEditable.toString();
String wechatId = wxidEditable.toString();
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) {
@@ -160,15 +258,29 @@ public class WriteMessageActivity extends AppCompatActivity {
Toaster.showLong("NFC卡片未连接");
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) {
Toaster.showLong("NFC卡片未连接");
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;
public class WechatContact implements Serializable {
public class ContactInfo implements Serializable {
private static final long serialVersionUID = -5570426400839799887L;
public String name;
public String phone;
public String tag;
public String wxid;
public String qq;
/**
* 0 电话 1 短信 2 微信语音 3 微信视频
* 1 电话 2 短信 3 微信语音 4 微信视频 5 QQ语音 6 QQ视频
*/
int operation;
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.phone = phone;
this.operation = 3;
@@ -47,7 +49,6 @@ public class WechatContact implements Serializable {
this.phone = phone;
}
public String getTag() {
return tag;
}
@@ -56,6 +57,22 @@ public class WechatContact implements Serializable {
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() {
return operation;
}
@@ -64,6 +81,14 @@ public class WechatContact implements Serializable {
this.operation = operation;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
@NonNull
@Override
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 {
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
Uri uri = Uri.parse("tel:" + phoneNumber);
// 创建URI类型的NdefRecord
NdefRecord uriRecord = NdefRecord.createUri(uri);
NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord});
try {
Ndef ndef = Ndef.get(tag);
ndef.connect();
ndef.writeNdefMessage(message); // 写入NDEF消息
ndef.close();
Toaster.show("写入联系人成功");
} catch (IOException | FormatException e) {
e.printStackTrace();
}
Ndef ndef = Ndef.get(tag);
ndef.connect();
ndef.writeNdefMessage(message); // 写入NDEF消息
ndef.close();
}
public static void writeUrlToTag(Tag tag, String url) {
public static void writeUrlToTag(Tag tag, String url) throws Exception {
// 确保URL包含协议如http://
Uri uri = Uri.parse(url);
NdefRecord uriRecord = NdefRecord.createUri(uri);
NdefMessage message = new NdefMessage(new NdefRecord[]{uriRecord});
try {
Ndef ndef = Ndef.get(tag);
ndef.connect();
ndef.writeNdefMessage(message);
ndef.close();
Toaster.show("写入Url成功");
} catch (IOException | FormatException e) {
e.printStackTrace();
}
Ndef ndef = Ndef.get(tag);
ndef.connect();
ndef.writeNdefMessage(message);
ndef.close();
}
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链接
Uri uri = Uri.parse(url);
Uri uri = Uri.parse(fixedUrl);
NdefRecord httpRecord = NdefRecord.createUri(uri);
// 2. 创建Android应用包名Record
// 2. 创建Android应用包名Record
NdefRecord androidRecord = NdefRecord.createApplicationRecord(pkg);
// 3. 按顺序组合记录
// 3. 按顺序组合记录
NdefMessage message = new NdefMessage(new NdefRecord[]{httpRecord, androidRecord});
// 4. 写入NFC标签
try {
Ndef ndef = Ndef.get(tag); // 获取标签对象
ndef.connect();
ndef.writeNdefMessage(message);
} catch (Exception e) {
Log.e(TAG, "writeUrlToTagIos: " + e.getMessage());
}
// 4. 写入NFC标签
Ndef ndef = Ndef.get(tag); // 获取标签对象
ndef.connect();
ndef.writeNdefMessage(message);
}
}

View File

@@ -11,8 +11,8 @@
type="com.ttstd.elderlyassistant.activity.nfc.NfcActivity.BtnClick" />
<variable
name="wechatContact"
type="com.ttstd.elderlyassistant.bean.WechatContact" />
name="contactInfo"
type="com.ttstd.elderlyassistant.bean.ContactInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -24,10 +24,10 @@
android:id="@+id/et_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:maxLength="15"
android:hint="Name"
android:inputType="text"
android:text="@{wechatContact.name}"
android:text="@{contactInfo.name}"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
@@ -38,12 +38,12 @@
android:id="@+id/et_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="phone"
android:maxLength="15"
android:hint="phoneNumber"
android:inputType="phone"
android:maxLines="1"
android:singleLine="true"
android:text="@{wechatContact.phone}"
android:text="@{contactInfo.phone}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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
name="click"
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>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent">
<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"
<LinearLayout
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
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" />
<RadioButton
android:id="@+id/rb_wechat_video"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==4}"
android:maxLines="1"
android:singleLine="true"
android:text="拨打微信视频" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::writeQqMusicForIos}"
android:text="QQ音乐兼容iPhone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_phone" />
<RadioButton
android:id="@+id/rb_qq_voice"
android:layout_width="match_parent"
android:layout_height="40dp"
android:checked="@{contactInfo.operation==5}"
android:maxLines="1"
android:singleLine="true"
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>
</layout>