前言
此博客只为记录自己工作思路,可能逻辑比较混乱,但是怕后面自己忘记项目过程中遇到的问题和解决思路,所以要先记下来,同时也便于自己梳理逻辑
利用系统dialer模块完成UI及数据存储
因为移动的任务比较急,只给了一周的时间,它的功能点包括拨号盘界面,联系人界面,通话记录界面等。如果全部自己做周期比较长,且遇到的问题也会比较多。所以我决定利用原生的dialer模块以减少工作量
-
dialer模块移出
系统的dialer模块与多个模块耦合在一起,如果只拷贝出dialer的源码,是无法通过编译的。所以模块的提取参考了博客
Android7.0 拨号盘应用源码分析(一) 界面浅析
代码链接是
AndroidDialer
各个工程的依赖关系
com.android.dialer是主工程依赖于
com.android.contacts.common工程和com.android.phone.common工程
com.android.contacts.common又依赖于
com.android.phone.common工程和com.android.common工程
除了dialer工程外其他工程都作为dialer的依赖(即在project structure里面对dialer add moudle dependency),且这些依赖工程在创建的时候都是作为lib创建的,manifest里面的activity啥的都是空的。
上面的工程可以通过编译,但是没有InCall的模块(即没有通话界面的模块)。所以我重新在源码里面将InCall模块的代码拷贝出来作为dependency(但是实际这里面只有InCall模块所需要的res文件,java文件我还是放在了dialer模块下的一个文件夹里面,原因是InCall模块和其他lib模块也相互依赖,如果将java文件也放在Incall模块里面,会导致因为相互依赖而无法通过编译。不将res文件也放到dialer模块的原因是不希望这两个模块的资源文件混在一起) -
UI调整
(1) ActionBarController
DialtactsActivity里面负责ActionBar的类是ActionBarController,负责了searchView的expand和collapse。我为了隐藏searchView添加了hideSearchBar的函数。另外如果要改Actionbar的高度,直接去res文件里面搜索action相关的名字即可修改
(2). FloatingActionButtonController
这个类在DialtactsActivity里面负责控制floating button的显示,包括颜色啊什么的。这些按钮包括DialtactsActivity里面的拨号按钮,联系人按钮和DialpadFragment里面的拨号按钮。我在这里面改了按钮的颜色
(3) ListsFragment
这个类包含了DialtactsActivity下面的viewpager,viewPager,以及所需的fragment:
CallLogFragment(通话记录),AllContactsFragment(联系人)。tab的icon就在这里面设置。
(4)IncallActivity相关的修改
来电界面的UI比较复杂,当时做的修改现在已经忘得差不多了,只记得主界面是InCallActivity,这个界面被start之后比较重要的函数是internalResolveIntent,我在这里面对intent传递过来的参数做了处理,包括判断是callin还是callout,以及name,number数据的传递。首先不管是呼入还是呼出电话,基础的fragment是CallCardFragment,这个是要调用showCallCardFragment(true) show出来的,而来电界面还要showAnswerFragment(true)。
<1> CallCardFragment与CallCardPresenter
CallCardFragment应该是通话界面上面那块包括一个挂断的按钮。具体的包括:
call_user,call_name,call_state,以及floating_end_call_action_button。end_button也是用的上述FloatingActionButtonController进行控制的。在onViewCreated对view进行初始化,通过setEndCallButtonEnabled对挂断电话的按钮进行控制。
CallCardPresenter相当于CallCardFragment的controller,里面定义了很多接口包括action和ui的init,而CallCardFragment去实现这些接口
<2> AnswerFragment与AnswerPresenter
AnswerFragment主要是来电界面下面的滑动按钮(滑动接听/挂断),它实现了AnswerPresenter中关于UI的接口。主要的功能完成还是在AnswerPresenter中,包括来电的处理,滑动icon的一些处理。
通话界面这个模块主要是参考一下博文:
来电界面流程分析
来电界面UI结构分析1
来电界面UI结构分析2
来电界面UI结构分析3
还有一个通话界面上DTMF按下后的keycode:
关于keyCode1
关于keyCode2 -
数据写入与读取
当时大致看了一下AllContactsFragment是继承ContactEntryListFragment,而数据加载是通过LoaderManager。一时脑抽,觉得原生的界面用的是原生的联系人db,我要操作原生数据库很麻烦,所以就自己写了一个界面和数据库。做完之后才想起来,原生的db可以通过ContentProvider去插入数据。界面只负责从原生contact db读取数据,而不需要知道这个数据是如何插入的。
那要对原生数据库进行增删改查就需要了解该db有哪些表和字段,以及db在data哪个目录下:
联系人db
/data/data/com.android.providers.contacts/databases/contacts2.db
通话记录db
/data/data/com.android.providers.contacts/databases/calllog.db
关于db的资料我参考了博文:
Android中联系人和通话记录详解(1)
最终我对原生数据库的增删改查是:
(1)插入通话记录
public static void insertCallRecord(Context context, CallRecord record) {
Log.i(TAG, "insertCallRecord: record:" + record);
ContentValues values = new ContentValues();
values.put(CallLog.Calls.CACHED_NAME, record.getmName());
values.put(CallLog.Calls.NUMBER, record.getmNumber());
//CallLog.Calls.OUTGOING_TYPE 外拨
//CallLog.Calls.INCOMING_TYPE 接入
//CallLog.Calls.MISSED_TYPE 未接
values.put(CallLog.Calls.TYPE, record.getmCallLogType());
values.put(CallLog.Calls.DATE, record.getmCallLogDate());
values.put(CallLog.Calls.NEW, "0");// 0已看1未看 ,由于没有获取默认全为已读
Log.e(TAG, "insertCallRecord: values:" + values.toString());
Uri uri = context.getContentResolver().insert(CallLog.Calls.CONTENT_URI, values);
Log.i(TAG, "insertCallRecord: uri:" + uri);
}
(2)清空联系人db
public static int clearSystemContactsDB() {
Log.i(TAG, "clearSystemContactsDB: ");
int status = 0;
try {
Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
DialerApplication.getContext().getContentResolver().delete(uri, "_id!=-1", null);
} catch (SQLException e) {
Log.e(TAG, "clearContactsDB: exception:" + e);
status = -1;
}
return status;
}
(3)插入联系人
/**
* 首先向RawContacts.CONTENT_URI执行一个空值插入,目的是获取系统返回的rawContactId
* 后面插入data表的数据,只有执行空值插入,才能使插入的联系人在通讯录里可见
*/
public static void insertContactToSystemDB(List<ContactInfo> contacts) {
try {
Log.i(TAG, "insertContactToSystemDB: contacts:" + contacts.toString());
ContentValues values = new ContentValues();
for (ContactInfo contact : contacts) {
values.clear();
Uri rawContactUri = DialerApplication.getContext().getContentResolver().insert(
ContactsContract.RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rawContactUri);
//往data表入姓名数据
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);// 内容类型
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.getNickname());
DialerApplication.getContext().getContentResolver().insert(ContactsContract.Data.CONTENT_URI,
values);
//往data表入电话数据
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, contact.getContactId());
values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
DialerApplication.getContext().getContentResolver().insert(android.provider.ContactsContract.Data.CONTENT_URI, values);
}
} catch (SQLException e) {
Log.e(TAG, "clearContactsDB: exception:" + e);
}
}
(4)根据contactid查找联系人(contactid就相当于number)
/**
* 根据contactId查找联系人
*
* @param context
* @param contactId
* @return
*/
public static ContactInfo getContactByContactIdFromSystemDB(Context context, String contactId) {
//uri= content://com.android.contacts/data/phones/filter/#
Cursor cursor = null;
try {
// Uri uri = Uri.parse("content://com.android.contacts/data/phones/filter/" + number);
//先判断该number是否为contactId
ContentResolver resolver = context.getContentResolver();
cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{ContactsContract.Data.DISPLAY_NAME},
ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?",
new String[]{contactId}, null); //从raw_contact表中返回display_name
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
String nickName = cursor.getString(0);
ContactInfo contact = new ContactInfo(Integer.parseInt(contactId), nickName);
return contact;
}
} catch (SQLException e) {
Log.e(TAG, "clearContactsDB: exception:" + e);
e.printStackTrace();
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
(5)根据姓名获得联系人信息
**
* 根据姓名获得contactInfo
*
* @param context
* @param nickName
* @return
*/
public static ContactInfo getContactByNickName(Context context, String nickName) {
Log.i(TAG, "getContactByNickName: nickName:" + nickName);
Cursor cursor = null;
ContactInfo contact = null;
try {
ContentResolver resolver = context.getContentResolver();
//view_data表中displayName相同的的值会有两行,一行的data1是number,一行是name
//根据mimeType区分。
//我的selection语句是:display_name = 'nickName' & mimeType = 'phone'
cursor = resolver.query(ContactsContract.Data.CONTENT_URI,
new String[]{"data1"},
ContactsContract.Data.DISPLAY_NAME + "= \'" + nickName + "\' and " +
ContactsContract.Data.MIMETYPE + "= \'" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "\'",
null, null);
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
int number = cursor.getInt(0);
Log.i(TAG, "getContactByNickName: number:" + number);
contact = new ContactInfo();
contact.setNickname(nickName);
contact.setContactId(number);
return contact;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return contact;
}
(6)根据row_contact_id找Contact
因为AllContactFragment里面的adapter获取的是row_contact_id。想要通过点击联系人打电话,需要通过row_contact_id查找联系人
/**
* 根据row_contact_id找Contact
*
* @param context
* @param id:row_contact_id
* @return
*/
public static ContactInfo getContactByRowContactIdFromSystemDB(Context context, int id) {
//uri= content://com.android.contacts/data/phones/filter/#
// Uri uri = Uri.parse("content://com.android.contacts/data" + number);
Cursor cursor = null;
ContactInfo contact = null;
try {
ContentResolver resolver = context.getContentResolver();
//从data表中返回display_name和contactId
//因为data1表中的数据会根据mimeType改变,所以对于同一个row_contact_id,会找到两行数据。
//一行data1是name,一行data1是number
cursor = resolver.query(ContactsContract.Data.CONTENT_URI,
new String[]{"data1", ContactsContract.Data.MIMETYPE},
ContactsContract.Data.RAW_CONTACT_ID + "=" + id,
null, null);
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
contact = new ContactInfo();
do {
String data1 = cursor.getString(0);
String mimeType = cursor.getString(1);
Log.i(TAG, "getContactByRowContactIdFromSystemDB: mimeType:" + mimeType + ",data1;" + data1);
if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
contact.setNickname(data1);
} else if (ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
contact.setContactId(Integer.parseInt(data1));
}
} while (cursor.moveToNext());
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return contact;
}
(7)获取通话记录里最后一个contactInfo用于重拨
/**
* 获得通话记录里最后一个contactInfo用于重拨
*
* @return
*/
@SuppressLint("MissingPermission")
public static ContactInfo getLastRecordContact(Context context) {
Cursor cursor = null;
ContactInfo contact = null;
try {
ContentResolver resolver = context.getContentResolver();
cursor = resolver.query(CallLog.Calls.CONTENT_URI, new String[]{CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER},
null, null, CallLog.Calls.DATE + " desc");
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
String name = cursor.getString(0);
String number = cursor.getString(1);
contact = new ContactInfo(Integer.parseInt(number), name);
Log.i(TAG, "getLastRecordContact: contact:" + contact);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return contact;
}
网络拉取同步数据
当用户在手机上通过移动的和家固话绑定/解绑智能音箱、设置音箱和家固话功能开启状态时,移动后台会通知我们的服务器数据更新。我们的服务器会推送消息给音箱,voip这边再解析服务器传输过来的数据(具体服务器是如何推送消息给音箱的,这块是其他同事做的,我并不清楚。我只要对接这个同事的接口完成IntentSercie接收他传来的状态数据就可以了)。
(1)manifest里面对接收数据的Service进行注册
<service android:name="com.kinstalk.her.cmccmode.data.OpenQPushMessageService">
<meta-data android:value="cmcc" android:name="topic"/>
<meta-data android:value="cmcc" android:name="group_topic"/>
<intent-filter>
<action android:name="android.intent.action.PUSH_MESSAGE" />
</intent-filter>
</service>
(2)对jason数据进行解析
@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent != null) {
String topic = intent.getStringExtra(TOPIC_EXTRA);
String message = intent.getStringExtra(MESSAGE_EXTRA);
onTextMessage(getApplicationContext(), topic, message);
}
}
// 收到应用内消息后回调此接口。
@SuppressLint("LongLogTag")
public void onTextMessage(Context context, String topic, String message) {
Log.e(TAG, "MessageReceiver::OnTextMessage : message is " + message);
JSONObject rspJson = JSONObject.parseObject(message);
AWSResponse responseObj = JSONObject.parseObject(rspJson.toJSONString(), AWSResponse.class);
Log.e(TAG, "onTextMessage: responseObj:" + responseObj.toString());
parseContent(context, responseObj);
}
@SuppressLint("LongLogTag")
private void parseContent(final Context context, AWSResponse response) {
if (AWS_TYPE_BIND.equals(response.getType())) {
……
//如果获取的是已经绑定的通知,在数据库里写入bind状态值,去服务器拉取key和secret,调用移动voip初始化接口并拉取联系人
Api.fetchKeySecret(new Api.KeySecretCallBack() {
@SuppressLint("LongLogTag")
@Override
public void getKeySecret(AppInfo appInfo) {
Log.e(TAG, "parseContent,getKeySecret from server success");
DeviceStatusUtil.setKeyAndSecret(context, appInfo);
Intent intent = new Intent(ACTION_GET_KS);
intent.putExtra(EXTRA_KEY, appInfo.getAppKey());
intent.putExtra(EXTRA_SECRET, appInfo.getAppsecret());
context.sendBroadcast(intent);
CmccService.initVoipAndFetchContacts2();
}
});
} else if (AWS_TYPE_UNBIND.equals(response.getType())) {
……
//如果获取的是解绑的通知,在数据库里写入bind状态值,调用移动voip注销接口
DeviceStatusUtil.setBindStatus(context, 0);
sendBindStatusReceiver(context, 0);
DeviceStatusUtil.setEnabledStatus(context, 0);
sendEnabledStatusReceiver(context, 0);
//登录信息,注册监听全部move掉
CmccService.logOut();
} else if (AWS_TYPE_CONTACT.equals(response.getType())) {
……
//如果获取的是联系人变更通知,则更新联系人
//fetch里面会clear
CmccService.startFetchContacts();
} else if (AWS_TYPE_DISABLE.equals(response.getType())) {
……
//如果获取的是禁用通知,在数据库写入状态值,调用注销接口,并语音播报“和家固话功能已禁用”
CmccService.initVoipAndFetchContacts2();
DeviceStatusUtil.setEnabledStatus(context, 1);
sendEnabledStatusReceiver(context, 1);
CmccService.reportTTS(context.getString(R.string.voip_enabled));
} else if (AWS_TYPE_ENABLE.equals(response.getType())) {
……
//如果获取的启用通知,在数据库写入状态值,调用移动voip初始化接口并拉取联系人,并语音播报“和家固话功能已启用”
CmccService.initVoipAndFetchContacts2();
DeviceStatusUtil.setEnabledStatus(context, 1);
sendEnabledStatusReceiver(context, 1);
CmccService.reportTTS(context.getString(R.string.voip_enabled));
} else if (AWS_TYPE_SECRET.equals(response.getType())) {
……
//如果获取的是secret变更通知,则重新将key,secret写入数据库
AppInfo appInfo = JSONObject.parseObject(response.getData(), AppInfo.class);
Log.i(TAG, "parseContent: appInfo:" + appInfo);
DeviceStatusUtil.setKeyAndSecret(context, appInfo);
Intent intent = new Intent(ACTION_GET_KS);
intent.putExtra(EXTRA_KEY, appInfo.getAppKey());
intent.putExtra(EXTRA_SECRET, appInfo.getAppsecret());
context.sendBroadcast(intent);
}
(3)用来拉取网络数据的类api
public class Api {
private static final String TAG = "Api";
public static String TEST_URL = "XXXXXXXXXXXXXXXXXXX";
public static String PRODUCT_URL = "XXXXXXXXXXXXXx";
private static final String GET_CONTACT_PATH = "XXXXXXX";
private static final String GET_APPKEY_PATH = "XXXXXXX";
/**
* 获取所有联系人
*
* @param callBack
*/
public static void fetchAllContacts(final AllContactsCallBack callBack) {
VoipThreadManager.getInstance().start(new Runnable() {
@Override
public void run() {
fetchContactPublic(null, callBack);
}
});
}
private static Request createAllContactsRequest() {
Log.i(TAG, "createPhoneNumbRequest");
HttpUrl httpUrl = new Request.Builder()
.url(PRODUCT_URL)
.build()
.url()
.newBuilder()
.addEncodedPathSegment(GET_CONTACT_PATH)
.addQueryParameter("sn", "83"+SystemTool.getMacForSn())
.build();
Log.i(TAG, "createPhoneNumbRequest:data = " + httpUrl.toString());
return new Request.Builder()
.url(httpUrl)
.get()//requestBody)
.build();
}
/**
* 根据nickName获取某个联系人的信息
*
* @param callBack
*/
public static void fetchContactByName(final String nickname, final AllContactsCallBack callBack) {
VoipThreadManager.getInstance().start(new Runnable() {
@Override
public void run() {
fetchContactPublic(nickname, callBack);
}
});
}
private static Request createContactRequest(String nickName) {
Log.i(TAG, "createPhoneNumbRequest");
HttpUrl httpUrl = new Request.Builder()
.url(PRODUCT_URL)
.build()
.url()
.newBuilder()
.addEncodedPathSegment(GET_CONTACT_PATH)
.addQueryParameter("sn", "83"+SystemTool.getMacForSn())
.addQueryParameter("nickname", nickName)
.build();
Log.i(TAG, "createPhoneNumbRequest:data = " + httpUrl.toString());
return new Request.Builder()
.url(httpUrl)
.get()//requestBody)
.build();
}
public interface AllContactsCallBack {
void getAllContacts(List<ContactInfo> contacts);
}
private static void fetchContactPublic(final String nickname, final AllContactsCallBack callBack) {
Log.i(TAG, "fetchContactPublic: Enter");
okhttp3.Response response;
try {
if (TextUtils.isEmpty(nickname)) {
response = OkhttpClientHelper.getOkHttpClient().newCall(createContactRequest(nickname)).execute();
} else {
response = OkhttpClientHelper.getOkHttpClient().newCall(createAllContactsRequest()).execute();
}
//判断请求是否成功
if (response == null) {
Log.e(TAG, "fetchContactPublic response empty");
} else if (!response.isSuccessful()) {
Log.e(TAG, "fetchContactPublic: response:" + response);
Log.e(TAG, "fetchContactPublic response failed");
} else {
if (!HttpHeaders.hasBody(response)) {
Log.e(TAG, "fetchContactPublic rspBody empty");
} else {
//打印服务端返回结果
ResponseBody rspBody = response.body();
long contentLength = rspBody.contentLength();
if (contentLength != 0) {
String sBody = rspBody.string();
Log.i(TAG, "fetchContactPublic body = : " + sBody);
try {
JSONObject rspJson = JSONObject.parseObject(sBody);
AllContactsResponse responseObj = JSONObject.parseObject(rspJson.toJSONString(), AllContactsResponse.class);
Log.i(TAG, "fetchContactPublic: responseObj:" + responseObj.toString());
callBack.getAllContacts(responseObj.getAllContacts());
} catch (JSONException e) {
callBack.getAllContacts(null);
e.printStackTrace();
}
}
}
}
} catch (IOException e) {
callBack.getAllContacts(null);
e.printStackTrace();
}
}
……
}
<1>因为获取网络数据是耗时操作,所以需要开启子线程,我用线程池来管理子线程
public class VoipThreadManager {
private static VoipThreadManager _instance;
private static int KEEP_ALIVE_TIME_IN_SECONDS = 10;
private static int QAI_POOL_SIZE = Runtime.getRuntime().availableProcessors();
private static int QAI_POOL_MAX_SIZE = 3;
private final BlockingQueue<Runnable> qaiTasksQueue = new LinkedBlockingQueue<>();
private ExecutorService qaiThreadPool;
private VoipThreadManager() {
// Creates a thread pool manager
try {
qaiThreadPool = new ThreadPoolExecutor(
QAI_POOL_SIZE, // Initial pool size
QAI_POOL_MAX_SIZE, // Max pool size
KEEP_ALIVE_TIME_IN_SECONDS,
TimeUnit.SECONDS,
qaiTasksQueue,
new QAIThreadFactory(Thread.MAX_PRIORITY - 1),
new CallerRunsPolicy());
} catch (Throwable t) {
qaiThreadPool = Executors.newCachedThreadPool();
}
}
public synchronized static VoipThreadManager getInstance() {
if (_instance == null) {
_instance = new VoipThreadManager();
}
return _instance;
}
public void start(Runnable runnable) {
qaiThreadPool.submit(runnable);
}
public <T> Future<T> start(Callable<T> callable) {
return qaiThreadPool.submit(callable);
}
/**
* 正常优先级的线程工厂
*/
private static class QAIThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private String namePrefix;
private int mPriority = Thread.NORM_PRIORITY;
QAIThreadFactory(int priority) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
mPriority = priority;
}
public Thread newThread(Runnable r) {
namePrefix = "cmccvoip-pool-" +
poolNumber.getAndIncrement() +
"-thread-";
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
t.setPriority(mPriority);
if (t.isDaemon())
t.setDaemon(false);
return t;
}
}
;
}
语音识别
这部分是要是要实现智能音箱识别用户所说指令,如打电话给姓名/号码,接听电话,挂断电话等命令。一开始我只处理aicore传过来的json数据来解析用户指令(aicore是另个同事对接的腾讯ai语音识别),在移动弱网环境下测试结果比较差,会出现识别错误或者无法使用aicore的情况。于是和同事讨论再加入本地识别(framework那边一直开audio监听用户说的话,如果出现了命令热词,就将语句传给我让我解析,我通过正则表达式提取name或number)。也就是说语音识别用了两种方式,aicore和本地识别,具体用哪个由fw那边判断,我这边只需要解析数据并调用voip接口。
(1)两种解析方式
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.i(TAG, "onHandleIntent: ");
if (intent != null) {
String voiceId = intent.getStringExtra(AICoreDef.AI_INTENT_SERVICE_VOICEID);
QLoveResponseInfo rspData = intent.getParcelableExtra(AICoreDef.AI_INTENT_SERVICE_INFO);
byte[] extendData = intent.getByteArrayExtra(AICoreDef.AI_INTENT_SERVICE_DATA);
//app在未注册到AICore时,处理相关的语音,后续流程会有注册动作,
//后续的处理工作在CmdCallback的回调方法handleQLoveResponseInfo中执行。
//解析aicore传来的数据
processData(voiceId, rspData, extendData);
String peterCmdString = intent.getStringExtra(PERTER_ACTION_CMD);
if (!TextUtils.isEmpty(peterCmdString)) {
//本地识别
selfDealCmd(peterCmdString);
}
}
}
(2)解析aicore传来的json数据,判断意图
private static void processData(String voiceId, QLoveResponseInfo qLoveResponseInfo, byte[] extendData) {
if (qLoveResponseInfo != null) {
XWResponseInfo xwResponseInfo = qLoveResponseInfo.xwResponseInfo;
String requestText = xwResponseInfo.requestText;//语音文本
Log.i(TAG, "processData: requestText:" + requestText);
XWAppInfo appInfo = xwResponseInfo.appInfo;
Log.i(TAG, "processData: appInfo:" + appInfo);
String skillId = appInfo.ID;
Log.i(TAG, "processData: skill-ID:" + skillId);
if (CMCC_VOIP_SKILL_ID.equals(skillId)) {
//腾讯可识别的
String rspData = xwResponseInfo.responseData;//JSONObject数据
Log.i(TAG, "processData: responseData:" + rspData);
try {
JSONObject jsonObj = new JSONObject(rspData);
if (jsonObj.has("intent" + "Name")) {
//获取意图:intentName
String intentName = jsonObj.optString("intentName");
Log.i(TAG, "processData: intetnName;" + intentName);
if (INTENT_MAKE_CALL.equals(intentName)) {
//打电话
……
} else if (INTENT_OPEN_CALL_LOG.equals(intentName)) {
//打开通话记录
Intent intent = new Intent(DialerApplication.getContext(), DialtactsActivity.class);
intent.putExtra(EXTRA_SHOW_TAB, TAB_INDEX_HISTORY);
DialerApplication.getContext().startActivity(intent);
} else if (INTENT_OPEN_CMCC_VOIP.equals(intentName)) {
//打开和家固话app
Intent intent = new Intent(DialerApplication.getContext(), DialtactsActivity.class);
DialerApplication.getContext().startActivity(intent);
} else if (INTENT_OPEN_CONTACTS.equals(intentName)) {
//打开联系人
Intent intent = new Intent(DialerApplication.getContext(), DialtactsActivity.class);
intent.putExtra(EXTRA_SHOW_TAB, TAB_INDEX_ALL_CONTACTS);
DialerApplication.getContext().startActivity(intent);
} else if (INTENT_OPEN_DIAPADS.equals(intentName)) {
//打开拨号盘
Intent intent = new Intent(DialerApplication.getContext(), DialtactsActivity.class);
intent.putExtra(EXTRA_SHOW_TAB, TAB_SHOW_DIAPAD);
DialerApplication.getContext().startActivity(intent);
} else if (INTENT_RECALL.equals(intentName)) {
//重拨/拨打上一个电话号码
ContactInfo contact = MyDBProviderHelper.getLastRecordContact(DialerApplication.getContext());
CmccService.callOutByNumber(contact.getContactId());
// DialerApplication.callOut(DialerApplication.getContext(), contact.getContactId());
} else if (INTENT_ACCEPTCALL.equals(intentName)) {
CmccService.voicePickUpCall();
} else if (INTENT_REJECTCALL.equals(intentName)) {
CmccService.voiceHangUpCall();
} else if (INTENT_UPDATELOG.equals(intentName)) {
CmccService.voiceUpdateLog();
}
}
} catch (JSONException e) {
e.printStackTrace();
}
} else {
//腾讯不能识别的打电话语义
selfDealCmd(requestText);
}
}
<1>其中的打电话逻辑:根据json数据里面的key判断用户说的是打电话给name还是number,不同的命令对应不同的函数调用。
if (jsonObj.has("slots")) {
JSONArray jsonArray = jsonObj.getJSONArray("slots");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
//根据key来解析所需的信息
String key = jsonObject.optString("key");
String value = jsonObject.optString("value");
Log.i(TAG, "processData: key:" + key + ",value:" + value);
if ("name".equals(key)) {
if (!TextUtils.isEmpty(value)) {
ArrayList<String> pinyinResult = PinYinTool.getPinyinArrayFromString(value);
Log.e(TAG, "processData: PinYinTool getPinyin:" + pinyinResult);
for (String py : pinyinResult) {
int j = 0;
for (; j < pinyinResult.size(); j++) {
Log.e(TAG, "processData: PinYinTool getPinyin:" + pinyinResult.get(i));
ContactInfo contact = MyDBProviderHelper.getContactInfoByPinYin(pinyinResult.get(i));
if (contact != null) {
CmccService.callOutByNameAndContactid(contact);
break;
}
}
if (j == pinyinResult.size()) {
Log.i(TAG, "processData: self skill,call out,no such contact");
CmccService.reportTTS(
String.format(DialerApplication.getContext().getString(R.string.voice_callout_no_contacts),
value));
}
}
}
} else if ("tel_number".equals(key)) {
if (!TextUtils.isEmpty(value)) {
Log.i(TAG, "processData: INTENT_MAKE_CALL by number:" + value);
CmccService.callOutByNumber(value);
}
} else if ("number".equals(key)) {
if (!TextUtils.isEmpty(value)) {
Log.i(TAG, "processData: INTENT_MAKE_CALL by number:" + value);
CmccService.callOutByNumber(value);
}
} else if ("person".equals(key)) {
if (!TextUtils.isEmpty(value)) {
Log.i(TAG, "processData: INTENT_MAKE_CALL by person:" + value);
CmccService.callOutByName(value);
}
}
}
}
<2>多音字名字的问题
如果命令是打电话给name,我调用pinyinTool去获取这个名字的拼音,并比对数据库中的拼音,如果拼音一致,说明存在该联系人,然后调用打电话的接口。(所以voip数据库有两个table,一个是原生dialer的,一个是我自己建的,用来保存从服务器获取到的联系人的姓名拼音)
关于姓名多音字的问题,我使用了第三方库pinyin4j获取每个汉字的拼音。如果名字里面有汉字是多音字,我通过拼接的方式获取所有可能的读音。比如“解小行”,得到的结果会是[jiexiaohang][xiexiaohang][jiexiaoxing][xiexiaoxing]。(算法效率应该不是很高,我只能保证逻辑正确,最后结果是对的。如果有朋友可以改进,也欢迎在评论区留言)。
/**
* 将字符串转为拼音,因为可能是多音字,那么得出的结果可能有多个
*
* @param text
* @return
*/
public static ArrayList<String> getPinyinArrayFromString(String text) {
//最后得到的结果
ArrayList<String> result = new ArrayList<>();
ArrayList<String> tempResult = new ArrayList();
//pinyin4j初始化
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
format.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
for (char c : text.toCharArray()) {
try {
//获得一个字符的拼音数组
String[] charPinYinArray = PinyinHelper.toHanyuPinyinStringArray(isPinYinChar(c), format);
//组合的结果,比如result里面有2个拼音string了,再拼接一个有n个拼音的字符,得到的结果是2n
int resultCount = charPinYinArray.length * result.size();
Log.i(TAG, "getPinyinArrayFromString: resultsize:" + result.size() + ",multipinyinlength:" + charPinYinArray.length);
//为了标记下标
int k = 0;
//因为size会随着内部的add和set不断变化,所以必须在for循环外部初始化好
int resultSize = result.size();
tempResult.clear();
tempResult = (ArrayList<String>) result.clone();
for (int i = 0; i <= resultSize; i++) {
//处理i=0的情况,刚开始的时候i=0,size=0,可能get不到值就设置onePinyin是"";如果i=size了就break
boolean firstPinyin = false;
if (i == 0 && resultSize == 0) {
firstPinyin = true;
} else if (i == resultSize) {
break;
}
//因为拼接后会直接set,所以需要把上一次的结果记下来,防止拼接用的是set过后的
//比如wu拼接多音字行,会设置0的位置结果为wuhang,第二次应该也是基于wu去拼接而不是wuhang
String lastPinYin = firstPinyin ? "" : tempResult.get(i);
Log.e(TAG, "getPinyinArrayFromString: lastPinYin:" + lastPinYin);
for (int j = 0; j < charPinYinArray.length; j++) {
Log.i(TAG, "getPinyinArrayFromString: multipinyin[" + j + "]:" + charPinYinArray[j]);
String onePinYin;
onePinYin = lastPinYin + charPinYinArray[j];
Log.i(TAG, "getPinyinArrayFromString: onePinYIn:"+onePinYin);
if (k < resultSize) {
result.set(k, onePinYin);
} else {
result.add(onePinYin);
}
k++;
}
}
} catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
badHanyuPinyinOutputFormatCombination.printStackTrace();
}
}
return result;
}
(3)本地识别传来的string通过正则匹配解析
//通过正则表达式自己去解析
public static void selfDealCmd(String requestText) {
CmdStructure cmd = requestMachRegex(requestText);
……
}
public static CmdStructure requestMachRegex(String requestText) {
ArrayList<RegexItem> regexList = new RegexList().contents;
int k = 0;
for (int num = 0; num < regexList.size(); num++) {
Pattern r = regexList.get(num).regex;
String[] tags = regexList.get(num).tags;
// 现在创建 matcher 对象
Matcher m = r.matcher(requestText);
if (m.find()) {
System.out.println("Pattern: " + r.pattern());
System.out.println(requestText + ">>> Much: " + m.group(0));
String key = null;
String value = null;
for (int seq = 1; seq <= tags.length; seq++) {
System.out.println(tags[seq - 1] + ": " + m.group(seq));
key = tags[seq - 1];
value = m.group(seq);
}
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
CmdStructure cmd;
if (!isValueNumber(value)) {
cmd = new CmdStructure(regexList.get(num).intentName, "name", value);
} else {
cmd = new CmdStructure(regexList.get(num).intentName, "number", value);
}
Log.i(TAG, "requestMachRegex: cmd:" + cmd);
return cmd;
}
k++;
} else {
System.out.println("NO MATCH: " + requestText);
}
}
return null;
}
<1>用于保存正则匹配结果的类
public class RegexItem {
public Pattern regex;
public String[] tags;
public String intentName;
public RegexItem(Pattern regex, String[] tags){
this.regex = regex;
this.tags = tags;
}
public RegexItem(Pattern regex, String[] tags, String intentName){
this.regex = regex;
this.tags = tags;
this.intentName = intentName;
}
}
<2>正则表达式list
public class RegexList {
public ArrayList<RegexItem> contents;
{
contents = new ArrayList<>();
contents.add(new RegexItem(Pattern.compile("打电话给+(.{1,})"),new String[]{"contact"},"makeCall"));
contents.add(new RegexItem(Pattern.compile("给(?:(.{1,}))+打电话"),new String[]{"contact"},"makeCall"));
//contents.add(new RegexItem(Pattern.compile(""), new String[]{}));
}
}
遇到的问题及解决方法
这个应用需要常驻后台,不被杀掉,因为如果应用被杀掉,就无法收到来电了。一开始是想在manifest里面设置persistent的属性,但是framework那边的同事不让这样做,因为原生的dialer很大,实际我只是利用原生dialer的界面,没有必要一直呆在后台。所以和同事讨论了一下,就是把dialer放在一个进程,然后写一个service放在另一个进程,service里面放移动的sdk调用并长期存活,由它去起dialer的界面,这样内存不足时可以去杀死dialer的进程。service与dialer两个进程间通信我用的广播,因为传递的是简单数据,姓名,号码等用来显示界面。
(1)Manifest声明Service是另外一个进程
<service
android:name="com.kinstalk.her.cmccmode.CmccService"
android:exported="true"
android:process=":cmccsdkService">
<intent-filter>
<action android:name="com.example.androidtest.myservice" />
</intent-filter>
</service>
(2)保持Service长存
<1>收到开机广播就startService
<2>onStartCommand 返回START_STICKY
<3>设置service优先级 android:priority = “1000”
<4>将service设为前台状态startForeground
<5>onDestroy的时候再startService
That’s All.
因为这个项目过了好久才总结,当初遇到的各种问题已经记不清了,有些东西又太琐碎,所以写的逻辑有点混乱。