蓝牙usecase通路切换(framework层)

在系统音频播放时,能够使用的通路有很多,而蓝牙使用的是a2dp(Advanced Audio Distribution Profile 蓝牙音频传输模型协定),A2DP是能够采用耳机内的芯片来堆栈数据,达到声音的高清晰度。然而并非支持A2DP的耳机就是蓝牙立体声耳机,立体声实现的基本要求是双声道,所以单声道的蓝牙耳机是不能实现立体声的。声音能达到44.1kHz,一般的耳机只能达到8kHz。如果手机支持蓝牙,只要装载A2DP协议,就能使用A2DP耳机了。还有消费者看到技术参数提到蓝牙V1.0 V1.1 V1.2 V2.0——这些是指蓝牙的技术版本,是指通过蓝牙传输的速度,他们是否支持A2DP具体要看蓝牙产品制造商是否使用这个技术。

在framework中有一个音频很关键的服务AudioService,这个服务主要承载着应用层的操作,其中蓝牙的操作也是经由这个类,首先来看下它的初始化,audioservice的开始起源于systemserverstartotherservice:

private void startOtherServices(@NonNull TimingsTraceAndSlog t) {           
        ...
        t.traceBegin("StartAudioService");
        if (!isArc) {
            mSystemServiceManager.startService(AudioService.Lifecycle.class);
        } else {
            String className = context.getResources()
                    .getString(R.string.config_deviceSpecificAudioService);
            try {
                mSystemServiceManager.startService(className + "$Lifecycle");
            } catch (Throwable e) {
                reportWtf("starting " + className, e);
            }
        }
        t.traceEnd();
        ...
 
}

先是systemserver启动,然后启动各种服务这里用的是SystemServiceManager(简称SSM)来startService,然后在SSM中通过反射来获取AudioService的子类Lifecycle,接着调用Lifecycle的构造方法,AudioService就被创建了,然后再调用LifecycleonStart方法,audioservice会被注册到servicemanager中,最后在系统准备好之后会调用到startBootPhase,这个函数对应到Lifecycle的onBootPhase,接着到AudioServicesystemReady。这就是AudioService的诞生过程,接着看AudioService的初始化:

// AudioService
public AudioService(Context context, AudioSystemAdapter audioSystem,
            SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper) {
        sLifecycleLogger.log(new AudioEventLogger.StringEvent("AudioService()"));
        // 开始获取一堆服务的代理
        mContext = context;
        mContentResolver = context.getContentResolver();
        mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
 
        mAudioSystem = audioSystem;
        mSystemServer = systemServer;
        mSettings = settings;
 
        mPlatformType = AudioSystem.getPlatformType(context);
 
        mIsSingleVolume = AudioSystem.isSingleVolume(context);
 
        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
        mSensorPrivacyManagerInternal =
                LocalServices.getService(SensorPrivacyManagerInternal.class);
 
        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
        mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
 
        mSfxHelper = new SoundEffectsHelper(mContext);
 
        mSpatializerHelper = new SpatializerHelper(this, mAudioSystem);
 
        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator();
        // 接着是获取一些音量,最大音量、最小音量、默认音量等等
        ...
 
 
        ...
        // device 代理
        mDeviceBroker = new AudioDeviceBroker(mContext, this);
 
        mRecordMonitor = new RecordingActivityMonitor(mContext);
        ...
        // 焦点控制
        mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor);
        ...
 
}
// AudioDeviceBroker
/*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
        mContext = context;
        mAudioService = service;
        // 这个蓝牙相关处理类
        mBtHelper = new BtHelper(this);
        mDeviceInventory = new AudioDeviceInventory(this);
        mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
 
        init();
    }
 
// BtHelper
    BtHelper(@NonNull AudioDeviceBroker broker) {
        mDeviceBroker = broker;
    }
// AudioDeviceInventory
/*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
        mDeviceBroker = broker;
        mAudioSystem = AudioSystemAdapter.getDefaultAdapter();
    }

上面代码列出来和蓝牙相关处理的类,接下来是systemready的时候看下这些类都有哪些处理:

systemReady() {
    sendMsg(mAudioHandler, MSG_SYSTEM_READY, SENDMSG_QUEUE,
                0, 0, null, 0);                                      ------> onSystemReady
 
public void onSystemReady() {
        mSystemReady = true;
        scheduleLoadSoundEffects();
 
        mDeviceBroker.onSystemReady();
...
}
// AudioDeviceBroker
/*package*/ void onSystemReady() {
        synchronized (mSetModeLock) {
            synchronized (mDeviceStateLock) {
                mModeOwnerPid = mAudioService.getModeOwnerPid();
                mBtHelper.onSystemReady();
            }
        }
    }
 
// BtHelper
    /*package*/ synchronized void onSystemReady() {
        mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
        if (AudioService.DEBUG_SCO) {
            Log.i(TAG, "In onSystemReady(), calling resetBluetoothSco()");
        }
        resetBluetoothSco();
        getBluetoothHeadset();
 
        //FIXME: this is to maintain compatibility with deprecated intent
        // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
        Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
        newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
                AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
        sendStickyBroadcastToAll(newIntent);
         
        // 通过BluetoothAdapter 注册了三个链接类型的回调A2DP/HEARING_AID/LE_AUDIO,我们只关注A2DP
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter != null) {
            // 这个是监听相关协议连接断开的标准函数
            adapter.getProfileProxy(mDeviceBroker.getContext(),
                    mBluetoothProfileServiceListener, BluetoothProfile.A2DP);
            adapter.getProfileProxy(mDeviceBroker.getContext(),
                    mBluetoothProfileServiceListener, BluetoothProfile.HEARING_AID);
            adapter.getProfileProxy(mDeviceBroker.getContext(),
                    mBluetoothProfileServiceListener, BluetoothProfile.LE_AUDIO);
        }
    }
// BtHelper
private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
            // 这个接口监听了服务的两种状态分别在服务连接和断开时会回调
            new BluetoothProfile.ServiceListener() {
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    if (AudioService.DEBUG_SCO) {
                        Log.i(TAG, "In onServiceConnected(), profile: " + profile +
                                   ", proxy: "+proxy);
                    }
                    switch(profile) {
                        case BluetoothProfile.A2DP:
                        case BluetoothProfile.A2DP_SINK:
                        case BluetoothProfile.HEADSET:
                        case BluetoothProfile.HEARING_AID:
                        case BluetoothProfile.LE_AUDIO:
                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                                    "BT profile service: connecting "
                                    + BluetoothProfile.getProfileName(profile) + " profile"));
                            mDeviceBroker.postBtProfileConnected(profile, proxy);
                            break;
 
                        default:
                            break;
                    }
                }
                public void onServiceDisconnected(int profile) {
                    Log.i(TAG, "In onServiceDisconnected(), profile: " + profile);
                    switch (profile) {
                        case BluetoothProfile.A2DP:
                        case BluetoothProfile.A2DP_SINK:
                        case BluetoothProfile.HEADSET:
                        case BluetoothProfile.HEARING_AID:
                        case BluetoothProfile.LE_AUDIO:
                        case BluetoothProfile.LE_AUDIO_BROADCAST:
                            mDeviceBroker.postBtProfileDisconnected(profile);
                            break;
 
                        default:
                            break;
                    }
                }
            };

在systemready之后BThelper便会去监听A2DP连接和断开device的状态,并把这两个状态会分别传递给音频处理分别通过函数mDeviceBroker.postBtProfileConnected(profile, proxy)和mDeviceBroker.postBtProfileDisconnected(profile),这两个函数实现是在AudioDeviceBroker,分别看下这两个函数的具体实现先看postBtProfileConnected:

// AudioDeviceBroker   
   /*package*/ void postBtProfileConnected(int profile, BluetoothProfile proxy) {
       sendILMsgNoDelay(MSG_IL_BT_SERVICE_CONNECTED_PROFILE, SENDMSG_QUEUE, profile, proxy);      ---->    BrokerHandler.handleMessage.what = MSG_IL_BT_SERVICE_CONNECTED_PROFILE
   }

   BrokerHandler.handleMessage {
       ...
       case MSG_IL_BT_SERVICE_CONNECTED_PROFILE:
                   // 此处我们参数是A2DP所以走IF
                   if (msg.arg1 != BluetoothProfile.HEADSET) {
                       synchronized (mDeviceStateLock) {
                           mBtHelper.onBtProfileConnected(msg.arg1, (BluetoothProfile) msg.obj);
                       }
                   } else {
                       synchronized (mSetModeLock) {
                           synchronized (mDeviceStateLock) {
                               mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
                           }
                       }
                   }
                   break;

       ...
   }

// BtHelper
/*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
       // 不是HEADSET
       if (profile == BluetoothProfile.HEADSET) {
           onHeadsetProfileConnected((BluetoothHeadset) proxy);
           return;
       }
       // A2DP
       if (profile == BluetoothProfile.A2DP) {
           mA2dp = (BluetoothA2dp) proxy;
       } else if (profile == BluetoothProfile.HEARING_AID) {
           mHearingAid = (BluetoothHearingAid) proxy;
       } else if (profile == BluetoothProfile.LE_AUDIO) {
           mLeAudio = (BluetoothLeAudio) proxy;
       }
       // 获取蓝牙连接的设备
       final List<BluetoothDevice> deviceList = proxy.getConnectedDevices();
       if (deviceList.isEmpty()) {
           return;
       }
       final BluetoothDevice btDevice = deviceList.get(0);
       // 获取设备状态是STATE_CONNECTED,已连接
       if (proxy.getConnectionState(btDevice) == BluetoothProfile.STATE_CONNECTED) {
           mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
                   new AudioDeviceBroker.BtDeviceChangedData(btDevice, null,
                       new BluetoothProfileConnectionInfo(profile),
                       "mBluetoothProfileServiceListener"));
       } else {
           mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
                   new AudioDeviceBroker.BtDeviceChangedData(null, btDevice,
                       new BluetoothProfileConnectionInfo(profile),
                       "mBluetoothProfileServiceListener"));
       }
   }

// AudioDeviceBroker
/*package*/ void queueOnBluetoothActiveDeviceChanged(@NonNull BtDeviceChangedData data) {
       if (data.mInfo.getProfile() == BluetoothProfile.A2DP && data.mPreviousDevice != null
               && data.mPreviousDevice.equals(data.mNewDevice)) {
           final String name = TextUtils.emptyIfNull(data.mNewDevice.getName());
           new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR
                   + "queueOnBluetoothActiveDeviceChanged_update")
                   .set(MediaMetrics.Property.NAME, name)
                   .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile())
                   .record();
           synchronized (mDeviceStateLock) {
               postBluetoothA2dpDeviceConfigChange(data.mNewDevice);
           }
       } else {
           // else 符合
           synchronized (mDeviceStateLock) {
               if (data.mPreviousDevice != null) {
                   btMediaMetricRecord(data.mPreviousDevice, MediaMetrics.Value.DISCONNECTED,
                           data);
                   sendLMsgNoDelay(MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE,
                           createBtDeviceInfo(data, data.mPreviousDevice,
                                   BluetoothProfile.STATE_DISCONNECTED));
               }
               // connect 连接状态
               if (data.mNewDevice != null) {
                   btMediaMetricRecord(data.mNewDevice, MediaMetrics.Value.CONNECTED, data);
                   sendLMsgNoDelay(MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE,
                           createBtDeviceInfo(data, data.mNewDevice,
                                   BluetoothProfile.STATE_CONNECTED));
               }
           }
       }
   }
// AudioDeviceBroker
               case MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT: {
                   final BtDeviceInfo info = (BtDeviceInfo) msg.obj;
                   if (info.mDevice == null) break;
                   AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                           "msg: onBluetoothActiveDeviceChange "
                                   + " state=" + info.mState
                                   // only querying address as this is the only readily available
                                   // field on the device
                                   + " addr=" + info.mDevice.getAddress()
                                   + " prof=" + info.mProfile
                                   + " supprNoisy=" + info.mSupprNoisy
                                   + " src=" + info.mEventSource
                                   )).printLog(TAG));
                   synchronized (mDeviceStateLock) {
                       mDeviceInventory.setBluetoothActiveDevice(info);
                   }
               } break;

// AudioDeviceInventory
   public int setBluetoothActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo info) {
       int delay;
       synchronized (mDevicesLock) {
           if (!info.mSupprNoisy
                   && (((info.mProfile == BluetoothProfile.LE_AUDIO
                       || info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST)
                       && info.mIsLeOutput)
                       || info.mProfile == BluetoothProfile.HEARING_AID
                       || info.mProfile == BluetoothProfile.A2DP)) {
               @AudioService.ConnectionState int asState =
                       (info.mState == BluetoothProfile.STATE_CONNECTED)
                               ? AudioService.CONNECTION_STATE_CONNECTED
                               : AudioService.CONNECTION_STATE_DISCONNECTED;
               delay = checkSendBecomingNoisyIntentInt(info.mAudioSystemDevice, asState,
                       info.mMusicDevice);
           } else {
               delay = 0;
           }

           if (AudioService.DEBUG_DEVICES) {
               Log.i(TAG, "setBluetoothActiveDevice device: " + info.mDevice
                       + " profile: " + BluetoothProfile.getProfileName(info.mProfile)
                       + " state: " + BluetoothProfile.getConnectionStateName(info.mState)
                       + " delay(ms): " + delay
                       + " codec:" + Integer.toHexString(info.mCodec)
                       + " suppressNoisyIntent: " + info.mSupprNoisy);
           }
           // 激活蓝牙设备
           mDeviceBroker.postBluetoothActiveDevice(info, delay);
           if (info.mProfile == BluetoothProfile.HEARING_AID
                   && info.mState == BluetoothProfile.STATE_CONNECTED) {
               mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, AudioSystem.FORCE_NONE,
                               "HEARING_AID set to CONNECTED");
           }
       }
       return delay;
   }

// AudioDeviceBroker
/*package*/ void postBluetoothActiveDevice(BtDeviceInfo info, int delay) {
       sendLMsg(MSG_L_SET_BT_ACTIVE_DEVICE, SENDMSG_QUEUE, info, delay);
   }

// AudioDeviceBroker.handleMessage
case MSG_L_SET_BT_ACTIVE_DEVICE:
                   synchronized (mDeviceStateLock) {
                       BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
                       mDeviceInventory.onSetBtActiveDevice(btInfo,
                               (btInfo.mProfile != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
                                       ? mAudioService.getBluetoothContextualVolumeStream()
                                       : AudioSystem.STREAM_DEFAULT);
                   }
                   break;
// AudioDeviceInventory
void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo, int streamType) {
       ...

       synchronized (mDevicesLock) {
           final String key = DeviceInfo.makeDeviceListKey(btInfo.mAudioSystemDevice, address);
           final DeviceInfo di = mConnectedDevices.get(key);

           final boolean isConnected = di != null;

           final boolean switchToUnavailable = isConnected
                   && btInfo.mState != BluetoothProfile.STATE_CONNECTED;
           final boolean switchToAvailable = !isConnected
                   && btInfo.mState == BluetoothProfile.STATE_CONNECTED;

           switch (btInfo.mProfile) {
               ...
               case BluetoothProfile.A2DP:
                   if (switchToUnavailable) {
                       // 蓝牙设备断开
                       makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
                   } else if (switchToAvailable) {
                       // device is not already connected
                       if (btInfo.mVolume != -1) {
                           mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
                                   // convert index to internal representation in VolumeStreamState
                                   btInfo.mVolume * 10, btInfo.mAudioSystemDevice,
                                   "onSetBtActiveDevice");
                       }
                       // 蓝牙设备连接
                       makeA2dpDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
                               "onSetBtActiveDevice", btInfo.mCodec);
                   }
                   break;
               ...
           }
       }
   }

以上代码是蓝牙设备通过A2DP连接之后触发音频这边的激活的过程,蓝牙断开的过程类似,最后他们都会到类AudioDeviceInventory中,由两个函数处理:makeA2dpDeviceAvailablemakeA2dpDeviceUnavailableNow,因为过程类似,断开的过程不予展示直接看这两个函数:

@GuardedBy("mDevicesLock")
    private void makeA2dpDeviceAvailable(String address, String name, String eventSource,
            int a2dpCodec) {
        // enable A2DP before notifying A2DP connection to avoid unnecessary processing in
        // audio policy manager
        // 激活蓝牙设备,最终是通过AudioSystem.setForceUse强制切换usecase
        mDeviceBroker.setBluetoothA2dpOnInt(true, true /*fromA2dp*/, eventSource);
        // at this point there could be another A2DP device already connected in APM, but it
        // doesn't matter as this new one will overwrite the previous one
        // 设置设备的连接状态,最后会到AudioPolicyManager中处理
        final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name),
                AudioSystem.DEVICE_STATE_AVAILABLE, a2dpCodec);
 
        // TODO: log in MediaMetrics once distinction between connection failure and
        // double connection is made.
        if (res != AudioSystem.AUDIO_STATUS_OK) {
            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                    "APM failed to make available A2DP device addr=" + address
                            + " error=" + res).printLog(TAG));
            // If error is audioserver died,add device to the list,so that during restart AS will
            // restore by triggering onRestoreDevices to add A2DP device to APM by calling
            // setDeviceConnection
            if (res != AudioSystem.AUDIO_STATUS_SERVER_DIED) {
                return;
            }
 
        } else {
            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                    "A2DP device addr=" + address + " now available").printLog(TAG));
        }
 
        // The convention for head tracking sensors associated with A2DP devices is to
        // use a UUID derived from the MAC address as follows:
        //   time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address
        UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(
                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address));
        final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
                address, a2dpCodec, sensorUuid);
        final String diKey = di.getKey();
        mConnectedDevices.put(diKey, di);
        // on a connection always overwrite the device seen by AudioPolicy, see comment above when
        // calling AudioSystem
        mApmConnectedDevices.put(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, diKey);
 
        mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
        setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
    }
 
    @GuardedBy("mDevicesLock")
    private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
                .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow");
 
        if (address == null) {
            mmi.set(MediaMetrics.Property.EARLY_RETURN, "address null").record();
            return;
        }
        final String deviceToRemoveKey =
                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
 
        mConnectedDevices.remove(deviceToRemoveKey);
        if (!deviceToRemoveKey
                .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
            // removing A2DP device not currently used by AudioPolicy, log but don't act on it
            AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                    "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
            mmi.set(MediaMetrics.Property.EARLY_RETURN,
                    "A2DP device made unavailable, was not used")
                    .record();
            return;
        }
 
        // device to remove was visible by APM, update APM
        mDeviceBroker.clearAvrcpAbsoluteVolumeSupported();
        final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address),
                AudioSystem.DEVICE_STATE_UNAVAILABLE, a2dpCodec);
 
        if (res != AudioSystem.AUDIO_STATUS_OK) {
            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                    "APM failed to make unavailable A2DP device addr=" + address
                            + " error=" + res).printLog(TAG));
            // TODO:  failed to disconnect, stop here
            // TODO: return;
        } else {
            AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                    "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
        }
        mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
        // Remove A2DP routes as well
        setCurrentAudioRouteNameIfPossible(null, true /*fromA2dp*/);
        mmi.record();
    }

上面两个函数其实使用的方法都差不多达到相反的目的,其最后的处理都是交由给AudioPolicyManager,可见apm在音频中扮演的角色应该是控制流的总负责,大部分控制流都要经过它。在激活蓝牙设备的时候会强制切换usecase,之后蓝牙设备的状态都是通过AudioSystem.setDeviceConnectionState来修改,可见这个函数在这一过程中也是扮演很重要的角色,至于它主要做了什么可能还要等后续做深入分析,目前蓝牙控制音频usecase的地方就这些,请待后续更新

你可能感兴趣的:(系统,网络,java,开发语言)