对接移动Voip

前言

此博客只为记录自己工作思路,可能逻辑比较混乱,但是怕后面自己忘记项目过程中遇到的问题和解决思路,所以要先记下来,同时也便于自己梳理逻辑

利用系统dialer模块完成UI及数据存储

因为移动的任务比较急,只给了一周的时间,它的功能点包括拨号盘界面,联系人界面,通话记录界面等。如果全部自己做周期比较长,且遇到的问题也会比较多。所以我决定利用原生的dialer模块以减少工作量

  1. 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模块的原因是不希望这两个模块的资源文件混在一起)

  2. 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>  CallCardFragmentCallCardPresenter
     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>  AnswerFragmentAnswerPresenter
     AnswerFragment主要是来电界面下面的滑动按钮(滑动接听/挂断),它实现了AnswerPresenter中关于UI的接口。主要的功能完成还是在AnswerPresenter中,包括来电的处理,滑动icon的一些处理。

    通话界面这个模块主要是参考一下博文:
    来电界面流程分析
    来电界面UI结构分析1
    来电界面UI结构分析2
    来电界面UI结构分析3
    还有一个通话界面上DTMF按下后的keycode:
    关于keyCode1
    关于keyCode2

  3. 数据写入与读取
     当时大致看了一下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.
因为这个项目过了好久才总结,当初遇到的各种问题已经记不清了,有些东西又太琐碎,所以写的逻辑有点混乱。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值