Android11 蓝牙同步通讯录

2022-12-15  本文已影响0人  yuLiangC

背景

车机开发使用了较新的版本,Android11,需要做蓝牙同步手机通讯录功能开发。结果发现Android7以后,很多framework提供的api都被阉割掉了,直接收回到了蓝牙系统进程里面。
以下所有介绍均是针对于Android11版本。

概述

Android系统已经实现了各个角色的功能代码。例如
蓝牙音乐:a2dp端(播放源端,通常是手机)和a2dp_sink端(被传输端,通常是车机)以及avrcp_target(播放源端,及被控制端,通常是手机)端和avrcp_controller端(被控制端,通常是车机)
蓝牙电话:hfp端(电话源端,通常是手机)和hfp_client端(被传输端,通常是车机)
蓝牙同步通讯录:pbap端(数据源端,通常是手机)和pbapclient端(被传输端,通常是车机)
源码是将设备作为数据源的一端,即手机的角色。假若想要将车机改造成数据传输端,则需要改源码。
源码目录:package/apps/Bluetooth
没错,其实就是系统蓝牙应用,这个应用随系统的启动而启动,相对于我们开发的第三方有关蓝牙功能的应用,蓝牙系统应用是作为service端存在,我们的第三方应用则作为client端存在。我们在调用framework相关api时都会通过binder机制将数据传输到Bluetooth应用,由它进行处理。

代码改动

更改设备角色通常只需要更改配置文件就行了,代码路径:
package/apps/Bluetooth/res/values/config.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009-2012 Broadcom Corporation
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
-->
<resources>
    <bool name="profile_supported_test">true</bool>
    <bool name="profile_supported_a2dp">false</bool>
    <bool name="profile_supported_a2dp_sink">true</bool>
    <bool name="profile_supported_hs_hfp">false</bool>
    <bool name="profile_supported_hfpclient">true</bool>
    <bool name="profile_supported_hid_host">true</bool>
    <bool name="profile_supported_opp">true</bool>
    <bool name="profile_supported_pan">true</bool>
    <bool name="profile_supported_pbap">false</bool>
    <bool name="profile_supported_gatt">true</bool>
    <bool name="pbap_include_photos_in_vcard">true</bool>
    <bool name="pbap_use_profile_for_owner_vcard">true</bool>
    <bool name="profile_supported_map">true</bool>
    <bool name="profile_supported_avrcp_target">false</bool>
    <bool name="profile_supported_avrcp_controller">true</bool>
    <bool name="profile_supported_sap">false</bool>
    <bool name="profile_supported_pbapclient">true</bool>
    <bool name="profile_supported_mapmce">false</bool>
    <bool name="profile_supported_hid_device">true</bool>

    <!-- If true, we will require location to be enabled on the device to
         fire Bluetooth LE scan result callbacks in addition to having one
         of the location permissions. -->
    <bool name="strict_location_check">true</bool>

    <!-- Specifies the min/max connection interval parameters for high priority,
         balanced and low power GATT configurations. These values are in
         multiples of 1.25ms. -->
    <integer name="gatt_high_priority_min_interval">9</integer>
    <integer name="gatt_high_priority_max_interval">12</integer>
    <!-- Default specs recommended interval is 30 (24 * 1.25) -> 50 (40 * 1.25)
         ms. -->
    <integer name="gatt_balanced_priority_min_interval">24</integer>
    <integer name="gatt_balanced_priority_max_interval">40</integer>
    <integer name="gatt_low_power_min_interval">80</integer>
    <integer name="gatt_low_power_max_interval">100</integer>

    <!-- Specifies latency parameters for high priority, balanced and low power
         GATT configurations. These values represents the number of packets a
         slave device is allowed to skip. -->
    <integer name="gatt_high_priority_latency">0</integer>
    <integer name="gatt_balanced_priority_latency">0</integer>
    <integer name="gatt_low_power_latency">2</integer>

    <bool name="headset_client_initial_audio_route_allowed">true</bool>

    <!-- @deprecated: use a2dp_absolute_volume_initial_threshold_percent
         instead. -->
    <integer name="a2dp_absolute_volume_initial_threshold">8</integer>

    <!-- AVRCP absolute volume initial value as percent of the maximum value.
         Valid values are in the interval [0, 100].
         Recommended value is 50. -->
    <integer name="a2dp_absolute_volume_initial_threshold_percent">50</integer>

    <!-- For A2DP sink ducking volume feature. -->
    <integer name="a2dp_sink_duck_percent">25</integer>
    <!-- If true, device requests audio focus and start avrcp updates on source start or play -->
    <bool name="a2dp_sink_automatically_request_audio_focus">true</bool>

    <!-- For enabling the AVRCP Controller Cover Artwork feature -->
    <bool name="avrcp_controller_enable_cover_art">true</bool>

    <!-- For enabling browsed cover art with the AVRCP Controller Cover Artwork feature -->
    <bool name="avrcp_controller_cover_art_browsed_images">true</bool>

    <!-- For enabling the hfp client connection service -->
    <bool name="hfp_client_connection_service_enabled">true</bool>

    <!-- For supporting emergency call through the hfp client connection service  -->
    <bool name="hfp_client_connection_service_support_emergency_call">true</bool>

    <!-- Enabling autoconnect over pan -->
    <bool name="config_bluetooth_pan_enable_autoconnect">true</bool>

    <!-- Enabling the phone policy -->
    <bool name="enable_phone_policy">true</bool>

    <!-- Configuring priorities of A2DP source codecs. Larger value means
         higher priority. Value -1 means the codec is disabled.
         Value 0 is reserved and should not be used here. Enabled codecs
         should have priorities in the interval [1, 999999], and each priority
         value should be unique. -->
    <integer name="a2dp_source_codec_priority_sbc">1001</integer>
    <integer name="a2dp_source_codec_priority_aac">2001</integer>
    <integer name="a2dp_source_codec_priority_aptx">3001</integer>
    <integer name="a2dp_source_codec_priority_aptx_hd">4001</integer>
    <integer name="a2dp_source_codec_priority_ldac">5001</integer>

    <!-- Package that is responsible for user interaction on pairing request,
         success or cancel.
         Receives:
          - BluetootDevice.ACTION_PAIRING_CANCEL on bond failure
          - BluetoothDevice.ACTION_PAIRING_REUQEST on pin request
          - BluetootDevice.ACTION_BOND_STATE_CHANGED on pairing request and success
          - BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST on access requests
          - BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL to cancel access requests -->
    <string name="pairing_ui_package">com.android.settings</string>

    <!-- Flag whether or not to keep polling AG with CLCC for call information every 2 seconds -->
    <bool name="hfp_clcc_poll_during_call">true</bool>

    <!-- Package that is providing the exposure notification service -->
    <string name="exposure_notification_package">com.google.android.gms</string>

</resources>

按照上面的描述,只需要将传输侧的相关配置打开就行了,例如,要将车机的同步通讯录功能打开,只需要将profile_supported_pbapclient配置改为true就行了。这里的代码都是已经改过之后的。
Android7.0之前版本的同步通讯录需要调用BluetoothPbapClient的相关api,连接上蓝牙之后进行传输和数据写入,相关的例子网上很多就不赘述了。这里只说新版本。
新版本的同步通讯录功能系统蓝牙都已经实现了,具体路径:
package/apps/Bluetooth/src/com/android/bluetooth/pbapclient/PbapClientStateMachine.java

-------以上代码省略
    class Connected extends State {
        @Override
        public void enter() {
            if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
                    BluetoothProfile.STATE_CONNECTED);
            mMostRecentState = BluetoothProfile.STATE_CONNECTED;
            if (mUserManager.isUserUnlocked()) {
                mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
                        .sendToTarget();
            }
        }

        @Override
        public boolean processMessage(Message message) {
            if (DBG) {
                Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
            }
            switch (message.what) {
                case MSG_DISCONNECT:
                    if ((message.obj instanceof BluetoothDevice)
                            && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
                        transitionTo(mDisconnecting);
                    }
                    break;

                case MSG_RESUME_DOWNLOAD:
                    mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
                            .sendToTarget();
                    break;

                default:
                    Log.w(TAG, "Received unexpected message while Connected");
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }
--------以下代码省略

可以看到,PbapClientStateMachine状态机在收到连接信息时会发送下载信息,handler收到后就会启动同步操作。
handler路径:package/apps/Bluetooth/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java

-------以上代码省略
            case MSG_DOWNLOAD:
                mAccountCreated = addAccount(mAccount);
                if (!mAccountCreated) {
                    Log.e(TAG, "Account creation failed.");
                    return;
                }
                if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) {
                    downloadContacts(FAV_PATH);
                }
                if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) {
                    downloadContacts(PB_PATH);
                }
                if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) {
                    downloadContacts(SIM_PB_PATH);
                }

                HashMap<String, Integer> callCounter = new HashMap<>();
                downloadCallLog(MCH_PATH, callCounter);
                downloadCallLog(ICH_PATH, callCounter);
                downloadCallLog(OCH_PATH, callCounter);
                break;

            default:
                Log.w(TAG, "Received Unexpected Message");
        }
        return;
--------以下代码省略

可以看到,收到download消息后,PbapClientConnectionHandler里面不止同步了通讯录,还同步了通话记录。
这样,在设置里面连上蓝牙后,开关几次同步通讯录开关后就能看到执行通讯录同步操作了,十几秒之后同步成功,查询车机上的通讯录数据库能看到数据已经存在了。
然而,同步成功我们应用端是没有办法知道的,这个时候就要改动一下源码了。最好的版本就是发送一个广播通知吧,在同步成功的地方发送一个同步成功的通知。具体时机是在同步通讯录成功还是同步通话记录成功就取决于自己了。
同步成功之后,本以为万事大吉了,然而设备不小心重启了,重启之后再次查询通讯录数据库,结果居然为空!重试了几次之后依然如此,监听了通讯录数据库后发现只要重启就会有变动通知发送过来,猜测某个地方肯定在重启时做了清空数据库的操作,源码里面搜了下CONTENT_URI,发现蓝牙进程里面做了delete操作,跟进去看:
package/apps/Bluetooth/src/com/android/bluetooth/pbapclient/PbapClientService.java

-------以上代码省略
    @Override
    protected boolean start() {
        if (VDBG) {
            Log.v(TAG, "onStart");
        }
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
        // delay initial download until after the user is unlocked to add an account.
        filter.addAction(Intent.ACTION_USER_UNLOCKED);
        // To remove call logs when PBAP was never connected while calls were made,
        // we also listen for HFP to become disconnected.
        filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
        try {
            registerReceiver(mPbapBroadcastReceiver, filter);
        } catch (Exception e) {
            Log.w(TAG, "Unable to register pbapclient receiver", e);
        }

        removeUncleanAccounts();
        registerSdpRecord();
        setPbapClientService(this);
        return true;
    }

    @Override
    protected boolean stop() {
        setPbapClientService(null);
        cleanUpSdpRecord();
        try {
            unregisterReceiver(mPbapBroadcastReceiver);
        } catch (Exception e) {
            Log.w(TAG, "Unable to unregister pbapclient receiver", e);
        }
        for (PbapClientStateMachine pbapClientStateMachine : mPbapClientStateMachineMap.values()) {
            pbapClientStateMachine.doQuit();
        }
        removeUncleanAccounts();
        return true;
    }

    void cleanupDevice(BluetoothDevice device) {
        if (DBG) Log.d(TAG, "Cleanup device: " + device);
        synchronized (mPbapClientStateMachineMap) {
            PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
            if (pbapClientStateMachine != null) {
                mPbapClientStateMachineMap.remove(device);
            }
        }
    }

    private void removeUncleanAccounts() {
        // Find all accounts that match the type "pbap" and delete them.
        AccountManager accountManager = AccountManager.get(this);
        Account[] accounts =
                accountManager.getAccountsByType(getString(R.string.pbap_account_type));
        if (VDBG) Log.v(TAG, "Found " + accounts.length + " unclean accounts");
        for (Account acc : accounts) {
            Log.w(TAG, "Deleting " + acc);
            try {
                getContentResolver().delete(CallLog.Calls.CONTENT_URI,
                        CallLog.Calls.PHONE_ACCOUNT_ID + "=?", new String[]{acc.name});
            } catch (IllegalArgumentException e) {
                Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
            }
            // The device ID is the name of the account.
            accountManager.removeAccountExplicitly(acc);
        }
    }
--------以下代码省略

泥马,原来每次启动PbapClientService和断开PbapClientService的时候都会做一次清理操作,难怪存不住,还有一个地方也很坑:

-------以上代码省略
   private void removeHfpCallLog(String accountName, Context context) {
        if (DBG) Log.d(TAG, "Removing call logs from " + accountName);
        // Delete call logs belonging to accountName==BD_ADDR that also match
        // component name "hfpclient".
        ComponentName componentName = new ComponentName(context, HfpClientConnectionService.class);
        String selectionFilter = CallLog.Calls.PHONE_ACCOUNT_ID + "=? AND "
                + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=?";
        String[] selectionArgs = new String[]{accountName, componentName.flattenToString()};
        try {
            getContentResolver().delete(CallLog.Calls.CONTENT_URI, selectionFilter, selectionArgs);
        } catch (IllegalArgumentException e) {
            Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
        }
    }
----------中间代码省略
    private class PbapBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (DBG) Log.v(TAG, "onReceive" + action);
            if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
                    disconnect(device);
                }
            } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
                for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
                    stateMachine.resumeDownload();
                }
            } else if (action.equals(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED)) {
                // PbapClientConnectionHandler has code to remove calllogs when PBAP disconnects.
                // However, if PBAP was never connected/enabled in the first place, and calls are
                // made over HFP, these calllogs will not be removed when the device disconnects.
                // This code ensures callogs are still removed in this case.
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);

                if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    if (DBG) {
                        Log.d(TAG, "Received intent to disconnect HFP with " + device);
                    }
                    // HFP client stores entries in calllog.db by BD_ADDR and component name
                    removeHfpCallLog(device.getAddress(), context);
                }
            }
        }
    }
--------以下代码省略

每次收到断开连接的消息时也会做一次清理数据操作,不止于此,状态机在收到断开消息时,也会做一次清理操作(PbapClientConnectionHandler),如下图:


image.png

这样做是为了安全考虑?不是太明白,这样的话每次同步成功之后只能当场用才有效,下次电话进来就不一定会显示联系人了。
想要长久保存的话将清理的代码注掉就好了。

上一篇 下一篇

猜你喜欢

热点阅读