Android蓝牙开发浅谈 __ 耳机录音

By | 08月18日
Advertisement

转自:http://www.eoeandroid.com/thread-18993-7-1.html

对于一般的软件开发人员来说,蓝牙是很少用到的,尤其是Android的蓝牙开发,国内的例子很少 Android对于蓝牙开发从2.0版本的sdk才开始支持,而且模拟器不支持,测试至少需要两部手机,所以制约了很多技术人员的开发,刚巧这段时间公司有蓝牙开发的需求,我看了很多国内、国外的资料,又研究了一下J2ME的蓝牙开发(为了找找思路),虽然我想要的功能还没实现(我曾经在很多论坛里问了很多遍,苦于没有高人解答..),我要实现的功能是连接一个硬件设备,凡是跟硬件沾上边的,都让软件人员开发头疼..


好了,废话不说了,鉴于很多开发人员现在也有蓝牙开发的需求,也为了大家少走些弯路,先将我积攒的一点点在Android蓝牙开发经验与大家分享一下!


首先,要操作蓝牙,先要在AndroidManifest.xml里加入权限


然后,看下api,Android所有关于蓝牙开发的类都在android.bluetooth包下,如下图,只有8个类

Android蓝牙开发浅谈  __ 耳机录音


而我们需要用到了就只有几个而已:

1.BluetoothAdapter 顾名思义,蓝牙适配器,直到我们建立bluetoothSocket连接之前,都要不断操作它

BluetoothAdapter里的方法很多,常用的有以下几个:

cancelDiscovery() 根据字面意思,是取消发现,也就是说当我们正在搜索设备的时候调用这个方法将不再继续搜索

disable()关闭蓝牙

enable()打开蓝牙,这个方法打开蓝牙不会弹出提示,更多的时候我们需要问下用户是否打开,一下这两行代码同样是打开蓝牙,不过会提示用户:

Intemtenabler=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivityForResult(enabler,reCode);//同startActivity(enabler);

getAddress()获取本地蓝牙地址

getDefaultAdapter()获取默认BluetoothAdapter,实际上,也只有这一种方法获取BluetoothAdapter

getName()获取本地蓝牙名称

getRemoteDevice(String address)根据蓝牙地址获取远程蓝牙设备

getState()获取本地蓝牙适配器当前状态(感觉可能调试的时候更需要)

isDiscovering()判断当前是否正在查找设备,是返回true

isEnabled()判断蓝牙是否打开,已打开返回true,否则,返回false

listenUsingRfcommWithServiceRecord(String name,UUID uuid)根据名称,UUID创建并返回BluetoothServerSocket,这是创建BluetoothSocket服务器端的第一步

startDiscovery()开始搜索,这是搜索的第一步

2.BluetoothDevice看名字就知道,这个类描述了一个蓝牙设备

createRfcommSocketToServiceRecord(UUIDuuid)根据UUID创建并返回一个BluetoothSocket


这个方法也是我们获取BluetoothDevice的目的——创建BluetoothSocket


这个类其他的方法,如getAddress(),getName(),同BluetoothAdapter

3.BluetoothServerSocket如果去除了Bluetooth相信大家一定再熟悉不过了,既然是Socket,方法就应该都差不多,


这个类一种只有三个方法


两个重载的accept(),accept(inttimeout)两者的区别在于后面的方法指定了过时时间,需要注意的是,执行这两个方法的时候,直到接收到了客户端的请求(或是过期之后),都会阻塞线程,应该放在新线程里运行!


还有一点需要注意的是,这两个方法都返回一个BluetoothSocket,最后的连接也是服务器端与客户端的两个BluetoothSocket的连接

close()这个就不用说了吧,翻译一下——关闭!

4.BluetoothSocket,跟BluetoothServerSocket相对,是客户端


一共5个方法,不出意外,都会用到

close(),关闭

connect()连接

getInptuStream()获取输入流

getOutputStream()获取输出流

getRemoteDevice()获取远程设备,这里指的是获取bluetoothSocket指定连接的那个远程蓝牙设备

1、获取本地蓝牙适配器

BluetoothAdapter
mAdapter= BluetoothAdapter.getDefaultAdapter();

2、打开蓝牙

if(!mAdapter.isEnabled()){

//弹出对话框提示用户是后打开

Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivityForResult(enabler, REQUEST_ENABLE);

//不做提示,强行打开

// mAdapter.enable();

}

3、搜索设备


1)
刚才说过了mAdapter.startDiscovery()

是第一步,可以你会发现没有返回的蓝牙设备,怎么知道查找到了呢?向下看,不要急

2)定义BroadcastReceiver,关于BroadcastReceiver不多讲了,不是今天的讨论内容,代码如下



BroadcastReceiver mReceiver = new BroadcastReceiver() {

public void onReceive(Context context, Intent intent) {


String action = intent.getAction();

//找到设备


if (BluetoothDevice.ACTION_FOUND.equals(action)) {

BluetoothDevice device = intent

.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

if (device.getBondState() != BluetoothDevice.BOND_BONDED) {

Log.v(TAG, "find device:" + device.getName()

+ device.getAddress());

}


}


//
搜索完成


else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED

.equals(action)) {


setTitle("
搜索完成");


if (mNewDevicesAdapter.getCount() == 0) {


Log.v(TAG,"find over");


}


}


//
执行更新列表的代码


}


};


这样,没当查找到新设备或是搜索完成,相应的操作都在上段代码的两个if里执行了,不过前提是你要先注册

BroadcastReceiver,具体代码如下


IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);


registerReceiver(mReceiver, filter);


filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);


registerReceiver(mReceiver, filter);


(这段代码,一般写在onCreate()里..)


3
建立连接,首先Android sdk(2.0以上版本)支持的蓝牙连接是通过BluetoothSocket建立连接(说的不对请高人指正),服务器端(BluetoothServerSocket)和客户端(BluetoothSocket)需指定同样的UUID,才能建立连接,因为建立连接的方法会阻塞线程,所以服务器端和客户端都应启动新线程连接

1)服务器端:


//UUID
格式一般是"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"可到

//http://www.uuidgenerator.com 申请


BluetoothServerSocket serverSocket = mAdapter. listenUsingRfcommWithServiceRecord(serverSocketName,UUID);


serverSocket.accept();


2)
客户端:


//
还记得我们刚才在BroadcastReceiver获取了BLuetoothDevice么?


BluetoothSocket clienSocket=dcvice. createRfcommSocketToServiceRecord(UUID);


clienSocket.connect();


4
、数据传递,通过以上操作,就已经建立的BluetoothSocket连接了,数据传递无非是通过流的形式


1
)获取流


inputStream = socket.getInputStream();


outputStream = socket.getOutputStream();


2
)写出、读入


这是基础的东西,在这就不多赘述了



终于写完了,这是我这两天的学习经验,希望对有蓝牙需求的朋友有所帮助!另外,之前我们提过

android.bluetooth下有8个类,还有4个类没有用到,那4个类里定义的都是常量,我也没用到它们..


最后把我找到的几个蓝牙的例子附在后面,希望从事软件开发,尤其是Android开发的朋友以后多沟通、多分享!

补充一下,使设备能够被搜索
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
startActivityForResult(enabler,REQUEST_DISCOVERABLE);

//===============================================================================================

android蓝牙耳机录音程序主要代码

一、初始化
添加权限:

......
private static String mFileName = null;
private MediaRecorder mRecorder = null;
private MediaPlayer mPlayer = null;
private AudioManager mAudioManager = null;
......
mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
......

二、录音
private void startRecording() {
//获得文件保存路径。记得添加android.permission.WRITE_EXTERNAL_STORAGE权限
mFileName = Environment.getExternalStorageDirectory().getAbsolutePath();
mFileName += "/btrecorder.3gp";

mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mRecorder.setOutputFile(mFileName);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
try {
mRecorder.prepare();//如果文件打开失败,此步将会出错。
} catch (IOException e) {
Log.e(LOG_TAG, "prepare() failed");
}

if(!mAudioManager.isBluetoothScoAvailableOffCall()){
Log.d(LOG_TAG, "系统不支持蓝牙录音");
return;
}
//蓝牙录音的关键,启动SCO连接,耳机话筒才起作用
mAudioManager.startBluetoothSco();
//蓝牙SCO连接建立需要时间,连接建立后会发出ACTION_SCO_AUDIO_STATE_CHANGED消息,通过接收该消息而进入后续逻辑。
//也有可能此时SCO已经建立,则不会收到上述消息,可以startBluetoothSco()前先stopBluetoothSco()
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);

if (AudioManager.SCO_AUDIO_STATE_CONNECTED == state) {
mAudioManager.setBluetoothScoOn(true); //打开SCO
mRecorder.start();//开始录音
unregisterReceiver(this); //别遗漏
}else{//等待一秒后再尝试启动SCO
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mAudioManager.startBluetoothSco();
}
}
}, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED));
}

三、停止录音
private void stopRecording() {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
if(mAudioManager.isBluetoothScoOn()){
mAudioManager.setBluetoothScoOn(false);
mAudioManager.stopBluetoothSco();
}
}

四、播放录音到A2DP
private void startPlaying() {
mPlayer = new MediaPlayer();
try {
if(!mAudioManager.isBluetoothA2dpOn()) mAudioManager.setBluetoothA2dpOn(true); //如果A2DP没建立,则建立A2DP连接
mAudioManager.stopBluetoothSco();//如果SCO没有断开,由于SCO优先级高于A2DP,A2DP可能无声音
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
mAudioManager.setStreamSolo(AudioManager.STREAM_MUSIC, true);
//让声音路由到蓝牙A2DP。此方法虽已弃用,但就它比较直接、好用。
mAudioManager.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_BLUETOOTH_A2DP, AudioManager.ROUTE_BLUETOOTH);
mPlayer.setDataSource(mFileName);
mPlayer.prepare();
mPlayer.start();
} catch (IOException e) {
Log.e(LOG_TAG, "prepare() failed");
}
}

五、停止A2DP播放
private void stopPlaying() {
mPlayer.release();
mPlayer = null;
mAudioManager.setStreamSolo(AudioManager.STREAM_MUSIC, false);
}

//===============================================================================================

手机蓝牙各类服务对应的UUID(常用的几个已通过验证)

ServiceDiscoveryServerServiceClassID_UUID = '{00001000-0000-1000-8000-00805F9B34FB}'
BrowseGroupDescriptorServiceClassID_UUID = '{00001001-0000-1000-8000-00805F9B34FB}'
PublicBrowseGroupServiceClass_UUID = '{00001002-0000-1000-8000-00805F9B34FB}'

#蓝牙串口服务
SerialPortServiceClass_UUID = '{00001101-0000-1000-8000-00805F9B34FB}'

LANAccessUsingPPPServiceClass_UUID = '{00001102-0000-1000-8000-00805F9B34FB}'

#拨号网络服务
DialupNetworkingServiceClass_UUID = '{00001103-0000-1000-8000-00805F9B34FB}'

#信息同步服务
IrMCSyncServiceClass_UUID = '{00001104-0000-1000-8000-00805F9B34FB}'

SDP_OBEXObjectPushServiceClass_UUID = '{00001105-0000-1000-8000-00805F9B34FB}'

#文件传输服务
OBEXFileTransferServiceClass_UUID = '{00001106-0000-1000-8000-00805F9B34FB}'

IrMCSyncCommandServiceClass_UUID = '{00001107-0000-1000-8000-00805F9B34FB}'
SDP_HeadsetServiceClass_UUID = '{00001108-0000-1000-8000-00805F9B34FB}'
CordlessTelephonyServiceClass_UUID = '{00001109-0000-1000-8000-00805F9B34FB}'
SDP_AudioSourceServiceClass_UUID = '{0000110A-0000-1000-8000-00805F9B34FB}'
SDP_AudioSinkServiceClass_UUID = '{0000110B-0000-1000-8000-00805F9B34FB}'
SDP_AVRemoteControlTargetServiceClass_UUID = '{0000110C-0000-1000-8000-00805F9B34FB}'
SDP_AdvancedAudioDistributionServiceClass_UUID = '{0000110D-0000-1000-8000-00805F9B34FB}'
SDP_AVRemoteControlServiceClass_UUID = '{0000110E-0000-1000-8000-00805F9B34FB}'
VideoConferencingServiceClass_UUID = '{0000110F-0000-1000-8000-00805F9B34FB}'
IntercomServiceClass_UUID = '{00001110-0000-1000-8000-00805F9B34FB}'

#蓝牙传真服务
FaxServiceClass_UUID = '{00001111-0000-1000-8000-00805F9B34FB}'

HeadsetAudioGatewayServiceClass_UUID = '{00001112-0000-1000-8000-00805F9B34FB}'
WAPServiceClass_UUID = '{00001113-0000-1000-8000-00805F9B34FB}'
WAPClientServiceClass_UUID = '{00001114-0000-1000-8000-00805F9B34FB}'

#个人局域网服务
PANUServiceClass_UUID = '{00001115-0000-1000-8000-00805F9B34FB}'

#个人局域网服务
NAPServiceClass_UUID = '{00001116-0000-1000-8000-00805F9B34FB}'

#个人局域网服务
GNServiceClass_UUID = '{00001117-0000-1000-8000-00805F9B34FB}'

DirectPrintingServiceClass_UUID = '{00001118-0000-1000-8000-00805F9B34FB}'
ReferencePrintingServiceClass_UUID = '{00001119-0000-1000-8000-00805F9B34FB}'
ImagingServiceClass_UUID = '{0000111A-0000-1000-8000-00805F9B34FB}'
ImagingResponderServiceClass_UUID = '{0000111B-0000-1000-8000-00805F9B34FB}'
ImagingAutomaticArchiveServiceClass_UUID = '{0000111C-0000-1000-8000-00805F9B34FB}'
ImagingReferenceObjectsServiceClass_UUID = '{0000111D-0000-1000-8000-00805F9B34FB}'
SDP_HandsfreeServiceClass_UUID = '{0000111E-0000-1000-8000-00805F9B34FB}'
HandsfreeAudioGatewayServiceClass_UUID = '{0000111F-0000-1000-8000-00805F9B34FB}'
DirectPrintingReferenceObjectsServiceClass_UUID = '{00001120-0000-1000-8000-00805F9B34FB}'
ReflectedUIServiceClass_UUID = '{00001121-0000-1000-8000-00805F9B34FB}'
BasicPringingServiceClass_UUID = '{00001122-0000-1000-8000-00805F9B34FB}'
PrintingStatusServiceClass_UUID = '{00001123-0000-1000-8000-00805F9B34FB}'

#人机输入服务
HumanInterfaceDeviceServiceClass_UUID = '{00001124-0000-1000-8000-00805F9B34FB}'

HardcopyCableReplacementServiceClass_UUID = '{00001125-0000-1000-8000-00805F9B34FB}'

#蓝牙打印服务
HCRPrintServiceClass_UUID = '{00001126-0000-1000-8000-00805F9B34FB}'

HCRScanServiceClass_UUID = '{00001127-0000-1000-8000-00805F9B34FB}'
CommonISDNAccessServiceClass_UUID = '{00001128-0000-1000-8000-00805F9B34FB}'
VideoConferencingGWServiceClass_UUID = '{00001129-0000-1000-8000-00805F9B34FB}'
UDIMTServiceClass_UUID = '{0000112A-0000-1000-8000-00805F9B34FB}'
UDITAServiceClass_UUID = '{0000112B-0000-1000-8000-00805F9B34FB}'
AudioVideoServiceClass_UUID = '{0000112C-0000-1000-8000-00805F9B34FB}'
SIMAccessServiceClass_UUID = '{0000112D-0000-1000-8000-00805F9B34FB}'
PnPInformationServiceClass_UUID = '{00001200-0000-1000-8000-00805F9B34FB}'
GenericNetworkingServiceClass_UUID = '{00001201-0000-1000-8000-00805F9B34FB}'
GenericFileTransferServiceClass_UUID = '{00001202-0000-1000-8000-00805F9B34FB}'
GenericAudioServiceClass_UUID = '{00001203-0000-1000-8000-00805F9B34FB}'
GenericTelephonyServiceClass_UUID = '{00001204-0000-1000-8000-00805F9B34FB}'

//==============================================================

Android 本身规定了 一系列的 蓝牙类型 在 BluetoothClass.Device 类里面 定义了一系列的类型 , 包括 耳机 车载设备

你扫描到蓝牙设备之后, 这些属性就是自带的, 你判断 BluetoothDevice 的类型与 BluetoothClass.Device 里面规定的类型对比即可找到对应类型, 这是 蓝牙厂商之间的约定类型

如 :

int    AUDIO_VIDEO_CAMCORDER   

int AUDIO_VIDEO_CAR_AUDIO   

int AUDIO_VIDEO_HANDSFREE   

int AUDIO_VIDEO_HEADPHONES  

int AUDIO_VIDEO_HIFI_AUDIO  

int AUDIO_VIDEO_LOUDSPEAKER 

int AUDIO_VIDEO_MICROPHONE
如何判断扫描到的蓝牙设备是不是蓝牙耳机.
if (device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) if (BluetoothDevice.ACTION_FOUND.equals(action)) { ////找到设备
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                if (device.getBondState() != BluetoothDevice.BOND_BONDED) { //如果未曾绑定, 则自动绑定
                    if (device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) //判断是蓝牙耳机.
                    {
                        edList.append("\r\n" + device.getName() + "=" + device.getBluetoothClass().getDeviceClass());

                    }
                }
            }
//===============================================================================================
Android跟蓝牙耳机建立连接有两种方式
1. Android 主动跟蓝牙耳机连BluetoothSettings 中和蓝牙耳机配对上之后, BluetoothHeadsetService 会收到BONDING_CREATED_ACTION,这个时候BluetoothHeadsetService 会主动去和蓝牙耳机建立RFCOMM 连接。
if (action.equals(BluetoothIntent.BONDING_CREATED_ACTION)) {
  if (mState == BluetoothHeadset.STATE_DISCONNECTED) {
  // Lets try and initiate an RFCOMM connection
   try {
    mBinder.connectHeadset(address, null);
   } catch (RemoteException e) {}
  }
} 

RFCOMM 连接的真正实现是在ConnectionThread 中,它分两步,第一步先通过SDPClient 查询蓝牙设备时候支持Headset 和Handsfree profile。
// 1) SDP query
SDPClient client = SDPClient.getSDPClient(address);
if (DBG) log("Connecting to SDP server (" + address + ")...");
if (!client.connectSDPAsync()) {
  Log.e(TAG, "Failed to start SDP connection to " + address);
  mConnectingStatusHandler.obtainMessage(SDP_ERROR).sendToTarget();
  client.disconnectSDP();
  return;
}
if (isInterrupted()) {
  client.disconnectSDP();
  return;
}
if (!client.waitForSDPAsyncConnect(20000)) { // 20 secs
  if (DBG) log("Failed to make SDP connection to " + address);
  mConnectingStatusHandler.obtainMessage(SDP_ERROR).sendToTarget();
  client.disconnectSDP();
  return;
}
if (DBG) log("SDP server connected (" + address + ")");
int headsetChannel = client.isHeadset();
if (DBG) log("headset channel = " + headsetChannel);
int handsfreeChannel = client.isHandsfree();
if (DBG) log("handsfree channel = " + handsfreeChannel);
client.disconnectSDP();

第2步才是去真正建立RFCOMM 连接。 // 2) RFCOMM connect mHeadset = new HeadsetBase(mBluetooth, address, channel); if (isInterrupted()) { return; } int result = mHeadset.waitForAsyncConnect(20000, // 20 secs mConnectedStatusHandler); if (DBG) log("Headset RFCOMM connection attempt took " +(System.currentTimeMillis() - timestamp) + " ms"); if (isInterrupted()) { return; } if (result

BluetoothHandsfree 会先做一些初始化工作,比如根据是Headset 还是Handsfree 初始化不同的ATParser,并且启动一个接收线程从已建立的RFCOMM上接收蓝牙耳机过来的控制命令(也就是AT 命令),接着判断如果是在打电话过程中,才去建立SCO 连接来打通数据通道。 /* package */ void connectHeadset(HeadsetBase headset, int headsetType) { mHeadset = headset; mHeadsetType = headsetType; if (mHeadsetType == TYPE_HEADSET) { initializeHeadsetAtParser(); } else { initializeHandsfreeAtParser(); } headset.startEventThread(); configAudioParameters(); if (inDebug()) { startDebug(); } if (isIncallAudio()) { audioOn(); } }

建立SCO 连接是通过SCOSocket 实现的 /** Request to establish SCO (audio) connection to bluetooth * headset/handsfree, if one is connected. Does not block. * Returns false if the user has requested audio off, or if there * is some other immediate problem that will prevent BT audio. */ /* package */ synchronized boolean audioOn() { mOutgoingSco = createScoSocket(); if (!mOutgoingSco.connect(mHeadset.getAddress())) { mOutgoingSco = null; } return true; } 当SCO 连接成功建立后,BluetoothHandsfree 会收到SCO_CONNECTED 消息,它就会去调用AudioManager 的setBluetoothScoOn函数,从而通知音频系统有个蓝牙耳机可用了。 到此,Android 完成了和蓝牙耳机的全部连接。 case SCO_CONNECTED: if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected()&&mConnectedSco == null) { if (DBG) log("Routing audio for outgoing SCO conection"); mConnectedSco = (ScoSocket)msg.obj; mAudioManager.setBluetoothScoOn(true); } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) { if (DBG) log("Rejecting new connected outgoing SCO socket"); ((ScoSocket)msg.obj).close(); mOutgoingSco.close(); } mOutgoingSco = null; break;

2. 蓝牙耳机主动跟Android 连首先BluetoothAudioGateway 会在一个线程中收到来自蓝牙耳机的RFCOMM 连接,然后发送消息给BluetoothHeadsetService。 mConnectingHeadsetRfcommChannel = -1; mConnectingHandsfreeRfcommChannel = -1; if(waitForHandsfreeConnectNative(SELECT_WAIT_TIMEOUT) == false) { if (mTimeoutRemainingMs > 0) { try { Log.i(tag, "select thread timed out, but " + mTimeoutRemainingMs + "ms of waiting remain."); Thread.sleep(mTimeoutRemainingMs); } catch (InterruptedException e) { Log.i(tag, "select thread was interrupted (2), exiting"); mInterrupted = true; } } } BluetoothHeadsetService 会根据当前的状态来处理消息,分3 种情况,第一是当前状态是非连接状态,会发送RFCOMM_CONNECTED 消息,后续处理请参见前面的分析。 case BluetoothHeadset.STATE_DISCONNECTED: // headset connecting us, lets join setState(BluetoothHeadset.STATE_CONNECTING); mHeadsetAddress = info.mAddress; mHeadset = new HeadsetBase(mBluetooth, mHeadsetAddress,info.mSocketFd,info.mRfcommChan,mConnectedStatusHandler); mHeadsetType = type; mConnectingStatusHandler.obtainMessage(RFCOMM_CONNECTED).sendToTarget(); break; 如果当前是正在连接状态, 则先停掉已经存在的ConnectThread,并直接调用BluetoothHandsfree 去建立SCO 连接。 case BluetoothHeadset.STATE_CONNECTING: // If we are here, we are in danger of a race condition // incoming rfcomm connection, but we are also attempting an // outgoing connection. Lets try and interrupt the outgoing // connection. mConnectThread.interrupt(); // Now continue with new connection, including calling callback mHeadset = new HeadsetBase(mBluetooth,mHeadsetAddress,info.mSocketFd,info.mRfcommChan,mConnectedStatusHandler); mHeadsetType = type; setState(BluetoothHeadset.STATE_CONNECTED,BluetoothHeadset.RESULT_SUCCESS); mBtHandsfree.connectHeadset(mHeadset,mHeadsetType); // Make sure that old outgoing connect thread is dead. break; 如果当前是已连接的状态,这种情况是一种错误case,所以直接断掉所有连接。

case BluetoothHeadset.STATE_CONNECTED: if (DBG) log("Already connected to " + mHeadsetAddress + ",disconnecting" +info.mAddress); mBluetooth.disconnectRemoteDeviceAcl(info.mAddress); break; 蓝牙耳机也可能会主动发起SCO 连接, BluetoothHandsfree 会接收到一个SCO_ACCEPTED消息,它会去调用AudioManager 的setBluetoothScoOn 函数,从而通知音频系统有个蓝牙耳机可用了。到此,蓝牙耳机完成了和Android 的全部连接。 case SCO_ACCEPTED: if (msg.arg1 == ScoSocket.STATE_CONNECTED) { if (isHeadsetConnected() && mAudioPossible && mConnectedSco ==null) { Log.i(TAG, "Routing audio for incoming SCO connection"); mConnectedSco = (ScoSocket)msg.obj; mAudioManager.setBluetoothScoOn(true); } else { Log.i(TAG, "Rejecting incoming SCO connection"); ((ScoSocket)msg.obj).close(); } } // else error trying to accept, try again mIncomingSco = createScoSocket(); mIncomingSco.accept(); break;

A2DP和AVRCP蓝牙音频传输协议的应用解释

A2DP全名是Advenced Audio Distribution Profile 蓝牙音频传输模型拹定。A2DP 规定了使用蓝牙非同步传输信道方式,传输高质量音乐文件数据的拹议堆栈软件和使用方法,基于该拹议就能通过以蓝牙方式传事输高品质的音乐了,例如可以利用立体声蓝牙耳机或蓝牙音响设备来收听音乐了。

AVRCP全名是Audio Video Remote Cortrol Profile音频/视频远程控制配置文件。AVRCP 设计用于提供控制 TV、Hi-fi 设备等的标准接口。此配置文件用于许可单个远程控制设备(或其它设备)控制所有用户可以接入的 A/V 设备。AVRCP 定义了如何控制流媒体的特征。包括暂停、停止、启动重放、音量控制及其它类型的远程控制操作。

AVRCP(Audio/Video Remote Control Profile)是一种在蓝牙协议栈A2DP/AVCTP上实现的控制技术,通俗点说,就是你用蓝牙耳机听歌时按一下拨号键它会暂停,按下选曲它会切换,这就是AVRCP的功劳。

A2DP和AVRCP是一对兄弟,A2DP里WM设备是控制端,蓝牙耳机是接收端,AVRCP反之,这里说的就是WM如何接收蓝牙耳机发送的AVRCP并处理的过程。

AVRCP是「Audio / Video Remote Control Profile」的缩写,也就是「蓝牙音讯和视讯遥控功能协定」。

AVRCP 是蓝牙功能的一种,能够让两个支援蓝牙音效传输的装置,例如︰行动电话和无线双耳耳机等,两者互相使用蓝牙连结,并且能够从耳机端以无线方式操作手机端的音乐播放:停止、播放、音量调整和前后跳曲目。 一般来说支援A2DP,几乎都会有支援AVRCP,在购买琳琅满目的蓝牙产品之前,不妨多留心看看产品盒装上的说明。

//=======================================================================================================

Programmatically connect to paired Bluetooth speaker and play audio

http://stackoverflow.com/questions/22226552/programmatically-connect-to-paired-bluetooth-speaker-and-play-audio

http://blog.csdn.net/qinjuning/article/details/6938436

Android中MediaButtonReceiver广播监听器的机制分析

在Android中并没有定义MediaButtonReceive这个广播类,MediaButtonReceive只是作为一种通俗的命名方式来响应

插入耳机后,点击耳机上的按钮(名称:MEDIA_BUTTON)接受该广播事件的类。所有该MEDIA_BUTTON的按下我们就简称

为MEDIA_BUTTON广播吧。

顾名思义:它显然是一个广播接收器类(BroadbcastReceiver),那么它就具备了BroadbcastReceiver类的使用方式,

但是,因为它需要通过AudioManager对象注册,所以它有着自己的独特之处(否则我也不会单独拿出来分析,- -),后面我们

会慢慢的讲解。

点击MEDIA_BUTTON发送的Intent Action 为:

ACTION_MEDIA_BUTTON ="android.intent.action.MEDIA_BUTTON"

Intent 附加值为(Extra)点击MEDIA_BUTTON的按键码 :

//获得KeyEvent对象

KeyEvent keyEvent = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);

//获得Action

String intentAction = intent.getAction() ;

AudioManager对象注册MEDIA_BUTTON广播的方法原型为:

public voidregisterMediaButtonEventReceiver(ComponentNameeventReceiver)

Register a component to be the sole receiverof MEDIA_BUTTON intents

Parameters:

eventReceiver : identifier of a BroadcastReceiver that will receive the media button intent. This broadcast receiver

must be declared in the application manifest.

从注释可知以下两点:

1、 在AudioManager对象注册一个MediaoButtonRecevie,使它成为MEDIA_BUTTON的唯一接收器(这很重要,

我们会放在后面讲解) 也就是说只有我能收到,其他的都收不到这个广播了,否则的话大家都收到会照成一定的混乱;

2、 该广播必须在AndroidManifest.xml文件中进行声明,否则就监听不到该MEDIA_BUTTON广播了。

下面我们就简单的写一个MediaButtonReceiver类,并且在AndroidManifest.xml定义

1、 自定义的MediaButtonReceiver 广播类

[java] view plaincopyprint?

  1. package com.qin.mediabutton;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.util.Log;
  6. import android.view.KeyEvent;
  7. public class MediaButtonReceiver extends BroadcastReceiver {
  8. private static String TAG = "MediaButtonReceiver";
  9. @Override
  10. public void onReceive(Context context, Intent intent) {
  11. // 获得Action
  12. String intentAction = intent.getAction();
  13. // 获得KeyEvent对象
  14. KeyEvent keyEvent = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
  15. Log.i(TAG, "Action ---->" + intentAction + " KeyEvent----->"+ keyEvent.toString());
  16. if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
  17. // 获得按键字节码
  18. int keyCode = keyEvent.getKeyCode();
  19. // 按下 / 松开 按钮
  20. int keyAction = keyEvent.getAction();
  21. // 获得事件的时间
  22. long downtime = keyEvent.getEventTime();
  23. // 获取按键码 keyCode
  24. StringBuilder sb = new StringBuilder();
  25. // 这些都是可能的按键码 , 打印出来用户按下的键
  26. if (KeyEvent.KEYCODE_MEDIA_NEXT == keyCode) {
  27. sb.append("KEYCODE_MEDIA_NEXT");
  28. }
  29. // 说明:当我们按下MEDIA_BUTTON中间按钮时,实际出发的是 KEYCODE_HEADSETHOOK 而不是
  30. // KEYCODE_MEDIA_PLAY_PAUSE
  31. if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == keyCode) {
  32. sb.append("KEYCODE_MEDIA_PLAY_PAUSE");
  33. }
  34. if (KeyEvent.KEYCODE_HEADSETHOOK == keyCode) {
  35. sb.append("KEYCODE_HEADSETHOOK");
  36. }
  37. if (KeyEvent.KEYCODE_MEDIA_PREVIOUS == keyCode) {
  38. sb.append("KEYCODE_MEDIA_PREVIOUS");
  39. }
  40. if (KeyEvent.KEYCODE_MEDIA_STOP == keyCode) {
  41. sb.append("KEYCODE_MEDIA_STOP");
  42. }
  43. // 输出点击的按键码
  44. Log.i(TAG, sb.toString());
  45. }
  46. }
  47. }
package com.qin.mediabutton;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.KeyEvent;

public class MediaButtonReceiver extends BroadcastReceiver {
    private static String TAG = "MediaButtonReceiver";
    @Override
    public void onReceive(Context context, Intent intent) {
        // 获得Action
        String intentAction = intent.getAction();
        // 获得KeyEvent对象
        KeyEvent keyEvent = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);

        Log.i(TAG, "Action ---->" + intentAction + "  KeyEvent----->"+ keyEvent.toString());

        if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
            // 获得按键字节码
            int keyCode = keyEvent.getKeyCode();
            // 按下 / 松开 按钮
            int keyAction = keyEvent.getAction();
            // 获得事件的时间
            long downtime = keyEvent.getEventTime();

            // 获取按键码 keyCode
            StringBuilder sb = new StringBuilder();
            // 这些都是可能的按键码 , 打印出来用户按下的键
            if (KeyEvent.KEYCODE_MEDIA_NEXT == keyCode) {
                sb.append("KEYCODE_MEDIA_NEXT");
            }
            // 说明:当我们按下MEDIA_BUTTON中间按钮时,实际出发的是 KEYCODE_HEADSETHOOK 而不是
            // KEYCODE_MEDIA_PLAY_PAUSE
            if (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE == keyCode) {
                sb.append("KEYCODE_MEDIA_PLAY_PAUSE");
            }
            if (KeyEvent.KEYCODE_HEADSETHOOK == keyCode) {
                sb.append("KEYCODE_HEADSETHOOK");
            }
            if (KeyEvent.KEYCODE_MEDIA_PREVIOUS == keyCode) {
                sb.append("KEYCODE_MEDIA_PREVIOUS");
            }
            if (KeyEvent.KEYCODE_MEDIA_STOP == keyCode) {
                sb.append("KEYCODE_MEDIA_STOP");
            }
            // 输出点击的按键码
            Log.i(TAG, sb.toString());
        }
    }
}

2、 在AndroidManifest.xml声明我们定义的广播类。

[java] view plaincopyprint?

  1. "MediaButtonReceiver">
  2. "android.intent.action.MEDIA_BUTTON">

在模拟器上,我们可以手动构造MEDA_BUTTON的广播,并且将它发送出去(后面会介绍)。

如果有真机测试的话,按下MEDIA_BUTTON是可以接受到MEDIA_BUTTON广播的,如果没有接受到,请关闭所有应用

程序,在观察效果。

继续我们的下一步分析:

前面我们说明通过registerMediaButtonEventReceiver(eventReceiver)方法注册时使它成为MEDIA_BUTTON的

唯一 接收器。这个唯一是怎么实现的呢? 我们在源码中,一步步追本溯源,相信一定可以找到答案,知道这“唯一“是

怎么来的。

第一步、 为AudioManager注册一个MediaButtonReceiver() ;

[java] view plaincopyprint?

  1. //获得AudioManager对象
  2. AudioManager mAudioManager =(AudioManager)getSystemService(Context.AUDIO_SERVICE);
  3. //构造一个ComponentName,指向MediaoButtonReceiver类
  4. //下面为了叙述方便,我直接使用ComponentName类来替代MediaoButtonReceiver类
  5. ComponentName mbCN = new ComponentName(getPackageName(),MediaButtonReceiver.class.getName());
  6. //注册一个MedioButtonReceiver广播监听
  7. mAudioManager.registerMediaButtonEventReceiver(mbCN);
  8. //取消注册的方法
  9. mAudioManager.unregisterMediaButtonEventReceiver(mbCN);
     //获得AudioManager对象
      AudioManager mAudioManager =(AudioManager)getSystemService(Context.AUDIO_SERVICE);
      //构造一个ComponentName,指向MediaoButtonReceiver类
      //下面为了叙述方便,我直接使用ComponentName类来替代MediaoButtonReceiver类
      ComponentName  mbCN = new ComponentName(getPackageName(),MediaButtonReceiver.class.getName());
      //注册一个MedioButtonReceiver广播监听
      mAudioManager.registerMediaButtonEventReceiver(mbCN);
      //取消注册的方法
      mAudioManager.unregisterMediaButtonEventReceiver(mbCN);

MediaButtonReceiver就是我们用来接收MEDIA_BUTTON的广播类,下面为了叙述方便和直观上得体验,我直接使用

ComponentName类来替代真正的MediaoButtonReceiver广播类。

说明 接下来分析的文件路径全部在 frameworks/base/media/java/android/media/ 下

第二步、 进入AudioManager.java进行查看 ,发现如下方法:

[java] view plaincopyprint?

  1. //注册的方法为:
  2. public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
  3. //TODO enforce the rule about the receiver being declared in the manifest
  4. //我们继续查看getService()方法,看看IAudioService类到底是什么?
  5. IAudioService service = getService();
  6. try {
  7. //只是简单的调用了service的方法来完成注册,继续跟踪
  8. service.registerMediaButtonEventReceiver(eventReceiver);
  9. } catch (RemoteException e) {
  10. Log.e(TAG, "Dead object in registerMediaButtonEventReceiver"+e);
  11. }
  12. }
  13. //取消注册的方法为
  14. public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
  15. IAudioService service = getService();
  16. try {
  17. //只是简单的调用了service的方法来取消注册,,继续跟踪
  18. service.unregisterMediaButtonEventReceiver(eventReceiver);
  19. } catch (RemoteException e) {
  20. Log.e(TAG, "Dead object in unregisterMediaButtonEventReceiver"+e);
  21. }
  22. }
     //注册的方法为:
      public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
            //TODO enforce the rule about the receiver being declared in the manifest
            //我们继续查看getService()方法,看看IAudioService类到底是什么?
             IAudioService service = getService();
            try {
                //只是简单的调用了service的方法来完成注册,继续跟踪
                service.registerMediaButtonEventReceiver(eventReceiver);

            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in registerMediaButtonEventReceiver"+e);
            }
      }
      //取消注册的方法为
      public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
            IAudioService service = getService();
            try {
                //只是简单的调用了service的方法来取消注册,,继续跟踪
                service.unregisterMediaButtonEventReceiver(eventReceiver);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in unregisterMediaButtonEventReceiver"+e);
            }
        }

找到getService()方法,其实现为:

[java] view plaincopyprint?

  1. //看看它到底是什么
  2. private static IAudioService getService()
  3. {
  4. // 单例模式,大家懂得
  5. if (sService != null) {
  6. return sService;
  7. }
  8. //了解Binder机制 以及AIDL文件的使用,就明白了这不过是通过AIDL文件定义的Java层Binder机制
  9. //b为IBinder基类接口
  10. IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
  11. //强制转换后,sService不过是一个客户端对象,IAudioService就是aidl文件定义的接口了
  12. sService = IAudioService.Stub.asInterface(b);
  13. return sService;
  14. }
  15. //sService对象的声明
  16. private static IAudioService sService; //单例模式,不足为奇了
     //看看它到底是什么
      private static IAudioService getService()
        {
                // 单例模式,大家懂得
            if (sService != null) {
                return sService;
           }
           //了解Binder机制 以及AIDL文件的使用,就明白了这不过是通过AIDL文件定义的Java层Binder机制
            //b为IBinder基类接口
            IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
           //强制转换后,sService不过是一个客户端对象,IAudioService就是aidl文件定义的接口了
            sService = IAudioService.Stub.asInterface(b);
           return sService;
        }
    //sService对象的声明
          private static IAudioService sService; //单例模式,不足为奇了

我们知道了AudiaoManager只不过是一个傀儡,所有的方法都是由IAudioService 对象去实现的,通过它的构造方式,

可以知道它应该是有AIDL文件形成的Binder机制, sService只是客户端对象,那么它的服务端对象在什么地方呢?

也就是继承了IAudioService.Stub桩的类。

第三步、接下来我们需要找到该IAudioService.aidl文件和真正的服务端对象

IAudioService.aidl定义如下:

[java] view plaincopyprint?

  1. package android.media;
  2. import android.content.ComponentName;
  3. import android.media.IAudioFocusDispatcher;
  4. /**
  5. * {@hide}
  6. */
  7. interface IAudioService {
  8. void adjustVolume(int direction, int flags);
  9. void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags);
  10. void adjustStreamVolume(int streamType, int direction, int flags);
  11. void setStreamVolume(int streamType, int index, int flags);
  12. void setStreamSolo(int streamType, boolean state, IBinder cb);
  13. void setStreamMute(int streamType, boolean state, IBinder cb);
  14. int getStreamVolume(int streamType);
  15. int getStreamMaxVolume(int streamType);
  16. void setRingerMode(int ringerMode);
  17. int getRingerMode();
  18. void setVibrateSetting(int vibrateType, int vibrateSetting);
  19. int getVibrateSetting(int vibrateType);
  20. boolean shouldVibrate(int vibrateType);
  21. void setMode(int mode, IBinder cb);
  22. int getMode();
  23. oneway void playSoundEffect(int effectType);
  24. oneway void playSoundEffectVolume(int effectType, float volume);
  25. boolean loadSoundEffects();
  26. oneway void unloadSoundEffects();
  27. oneway void reloadAudioSettings();
  28. void setSpeakerphoneOn(boolean on);
  29. boolean isSpeakerphoneOn();
  30. void setBluetoothScoOn(boolean on);
  31. boolean isBluetoothScoOn();
  32. int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, IAudioFocusDispatcher l, String clientId);
  33. int abandonAudioFocus(IAudioFocusDispatcher l, String clientId);
  34. void unregisterAudioFocusClient(String clientId);
  35. void registerMediaButtonEventReceiver(in ComponentName eventReceiver); //这个方法是我们需要弄懂的
  36. void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver); //这个方法也是是我们需要弄懂的
  37. void startBluetoothSco(IBinder cb);
  38. void stopBluetoothSco(IBinder cb);
  39. }
    package android.media;

    import android.content.ComponentName;
    import android.media.IAudioFocusDispatcher;
    /**
     * {@hide}
     */
    interface IAudioService {

        void adjustVolume(int direction, int flags);
        void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags);
        void adjustStreamVolume(int streamType, int direction, int flags);
        void setStreamVolume(int streamType, int index, int flags);
        void setStreamSolo(int streamType, boolean state, IBinder cb);
        void setStreamMute(int streamType, boolean state, IBinder cb);
        int getStreamVolume(int streamType);
        int getStreamMaxVolume(int streamType);
        void setRingerMode(int ringerMode);
        int getRingerMode();
        void setVibrateSetting(int vibrateType, int vibrateSetting);
        int getVibrateSetting(int vibrateType);
        boolean shouldVibrate(int vibrateType);
        void setMode(int mode, IBinder cb);
        int getMode();
        oneway void playSoundEffect(int effectType);
        oneway void playSoundEffectVolume(int effectType, float volume);
        boolean loadSoundEffects();
        oneway void unloadSoundEffects();
        oneway void reloadAudioSettings();
        void setSpeakerphoneOn(boolean on);
        boolean isSpeakerphoneOn();
        void setBluetoothScoOn(boolean on);
        boolean isBluetoothScoOn();
        int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, IAudioFocusDispatcher l, String clientId);
        int abandonAudioFocus(IAudioFocusDispatcher l, String clientId);
        void unregisterAudioFocusClient(String clientId);
        void registerMediaButtonEventReceiver(in ComponentName eventReceiver);   //这个方法是我们需要弄懂的
        void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver);  //这个方法也是是我们需要弄懂的
        void startBluetoothSco(IBinder cb);
        void stopBluetoothSco(IBinder cb);
    }

真正的服务端对象就是继承了 IAudioService.Stub 桩的类,AudioService就是该服务端对象,其实AudioManager的

所有操作都是由AudioService来实现的,它才是真正的老大。

第五步、 AudioService.java

[java] view plaincopyprint?

  1. //AudioService类
  2. public class AudioService extends IAudioService.Stub {
  3. //.....
  4. //仅仅列出我们需要的方法
  5. //这儿才是真正的注册MediaButtonReceiver的方法
  6. public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
  7. Log.i(TAG, " Remote Control registerMediaButtonEventReceiver() for " + eventReceiver);
  8. synchronized(mRCStack) {
  9. //调用它去实现注册ComponentName
  10. pushMediaButtonReceiver(eventReceiver);
  11. }
  12. }
  13. //在查看pushMediaButtonReceiver()方法 先理解一下两个知识点,很重要的。
  14. //RemoteControlStackEntry内部类不过是对ComponentName类的进一步封装(感觉没必要在加一层进行封装了)
  15. private static class RemoteControlStackEntry {
  16. public ComponentName mReceiverComponent;// 属性
  17. //TODO implement registration expiration?
  18. //public int mRegistrationTime;
  19. public RemoteControlStackEntry() {
  20. }
  21. public RemoteControlStackEntry(ComponentName r) {
  22. mReceiverComponent = r;// 构造函数赋值给mReceiverComponent对象
  23. }
  24. }
  25. //采用了栈存储结构(先进后出)来保存所有RemoteControlStackEntry对象,也就是保存了ComponentName对象
  26. private Stack mRCStack = new Stack();
  27. //回到pushMediaButtonReceiver()查看,这下该拨开云雾了吧,继续学习
  28. private void pushMediaButtonReceiver(ComponentName newReceiver) {
  29. // already at top of stack?
  30. //采用了一个栈(前面我们介绍的知识点)来保存所有注册的ComponentName对象
  31. //如果当前栈不为空并且栈顶的对象与新注册的ComponentName对象一样,不做任何事,直接返回
  32. if (!mRCStack.empty() && mRCStack.peek().mReceiverComponent.equals(newReceiver)) {
  33. return;
  34. }
  35. //获得mRCStack栈的迭代器
  36. Iterator stackIterator = mRCStack.iterator();
  37. //循环
  38. while(stackIterator.hasNext()) {
  39. RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
  40. //如果当前栈内保存该新注册的ComponentName对象,将它移除,跳出循环
  41. if(rcse.mReceiverComponent.equals(newReceiver)) {
  42. mRCStack.remove(rcse);
  43. break;
  44. }
  45. }
  46. //将新注册的ComponentName对象放入栈顶
  47. mRCStack.push(new RemoteControlStackEntry(newReceiver));
  48. }
  49. }
    //AudioService类
    public class AudioService extends IAudioService.Stub {
        //.....
        //仅仅列出我们需要的方法
        //这儿才是真正的注册MediaButtonReceiver的方法
        public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
            Log.i(TAG, "  Remote Control   registerMediaButtonEventReceiver() for " + eventReceiver);

            synchronized(mRCStack) {
              //调用它去实现注册ComponentName
                pushMediaButtonReceiver(eventReceiver);
            }
        }

       //在查看pushMediaButtonReceiver()方法  先理解一下两个知识点,很重要的。
        //RemoteControlStackEntry内部类不过是对ComponentName类的进一步封装(感觉没必要在加一层进行封装了)
        private static class RemoteControlStackEntry {
            public ComponentName mReceiverComponent;// 属性
              //TODO implement registration expiration?
            //public int mRegistrationTime;

            public RemoteControlStackEntry() {
            }

            public RemoteControlStackEntry(ComponentName r) {
                mReceiverComponent = r;// 构造函数赋值给mReceiverComponent对象
            }
        }

       //采用了栈存储结构(先进后出)来保存所有RemoteControlStackEntry对象,也就是保存了ComponentName对象
        private Stack mRCStack = new Stack();

       //回到pushMediaButtonReceiver()查看,这下该拨开云雾了吧,继续学习
       private void pushMediaButtonReceiver(ComponentName newReceiver) {
         // already at top of stack?
            //采用了一个栈(前面我们介绍的知识点)来保存所有注册的ComponentName对象
            //如果当前栈不为空并且栈顶的对象与新注册的ComponentName对象一样,不做任何事,直接返回
            if (!mRCStack.empty() && mRCStack.peek().mReceiverComponent.equals(newReceiver)) {
                return;
            }
            //获得mRCStack栈的迭代器
            Iterator stackIterator = mRCStack.iterator();
            //循环
            while(stackIterator.hasNext()) {
              RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
              //如果当前栈内保存该新注册的ComponentName对象,将它移除,跳出循环
                if(rcse.mReceiverComponent.equals(newReceiver)) {
                    mRCStack.remove(rcse);
                    break;
                }
            }
          //将新注册的ComponentName对象放入栈顶
            mRCStack.push(new RemoteControlStackEntry(newReceiver));
        }
    }

小结一下:


栈(mRCStack)维护了所有CompoentName对象,对每个CompoentName对象,保证它有且仅有一个,

新注册的CompoentName对象永远处于栈顶

我们看下取消注册的方法:

[java] view plaincopyprint?

  1. //我们看下取消注册的方法
  2. /** see AudioManager.unregisterMediaButtonEventReceiver(ComponentName eventReceiver) */
  3. public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
  4. Log.i(TAG, " Remote Control unregisterMediaButtonEventReceiver() for " + eventReceiver);
  5. synchronized(mRCStack) {
  6. //调用removeMediaButtonReceiver方法去实现
  7. removeMediaButtonReceiver(eventReceiver);
  8. }
  9. }
  10. private void removeMediaButtonReceiver(ComponentName newReceiver) {
  11. Iterator stackIterator = mRCStack.iterator();
  12. while(stackIterator.hasNext()) {
  13. //获得mRCStack栈的迭代器
  14. RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
  15. //如果存在该对象,则移除,跳出循环
  16. if(rcse.mReceiverComponent.equals(newReceiver)) {
  17. mRCStack.remove(rcse);
  18. break;
  19. }
  20. }
  21. }
    //我们看下取消注册的方法
    /** see AudioManager.unregisterMediaButtonEventReceiver(ComponentName eventReceiver) */
    public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
        Log.i(TAG, "  Remote Control   unregisterMediaButtonEventReceiver() for " + eventReceiver);

        synchronized(mRCStack) {
             //调用removeMediaButtonReceiver方法去实现
            removeMediaButtonReceiver(eventReceiver);
        }
    }

    private void removeMediaButtonReceiver(ComponentName newReceiver) {
        Iterator stackIterator = mRCStack.iterator();
        while(stackIterator.hasNext()) {
             //获得mRCStack栈的迭代器
            RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
            //如果存在该对象,则移除,跳出循环
            if(rcse.mReceiverComponent.equals(newReceiver)) {
                mRCStack.remove(rcse);
                break;
            }
        }
    }

通过对前面的学习,我们知道了AudioManager内部利用一个栈来管理包括加入和移除ComponentName对象,

新的疑问来了?这个MEDIA_BUTTON广播是如何分发的呢 ?

其实,AudioService.java文件中也存在这么一个MediaoButtonReceiver的广播类,它为系统广播接收器,即用来接收

系统的MEDIA_BUTTON广播,当它接收到了这个MEDIA_BUTTON广播 ,它会对这个广播进行进一步处理,这个处理过程

就是我们需要的弄清楚。

MediaButtonBroadcastReceiver 内部类如下:

[java] view plaincopyprint?

  1. private class MediaButtonBroadcastReceiver extends BroadcastReceiver {
  2. @Override
  3. public void onReceive(Context context, Intent intent) {
  4. //获得action ,系统MEDIA_BUTTON广播来了
  5. String action = intent.getAction();
  6. //action不正确 直接返回
  7. if (!Intent.ACTION_MEDIA_BUTTON.equals(action)) {
  8. return;
  9. }
  10. //获得KeyEvent对象
  11. KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
  12. if (event != null) {
  13. // if in a call or ringing, do not break the current phone app behavior
  14. // TODO modify this to let the phone app specifically get the RC focus
  15. // add modify the phone app to take advantage of the new API
  16. //来电或通话中,不做处理直接返回
  17. if ((getMode() == AudioSystem.MODE_IN_CALL) ||(getMode() == AudioSystem.MODE_RINGTONE)) {
  18. return;
  19. }
  20. synchronized(mRCStack) {
  21. //栈不为空
  22. if (!mRCStack.empty()) {
  23. // create a new intent specifically aimed at the current registered listener
  24. //构造一个Intent对象 ,并且赋予Action和KeyEvent
  25. Intent targetedIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
  26. targetedIntent.putExtras(intent.getExtras());
  27. //指定该处理Intent的对象为栈顶ComponentName对象的广播类
  28. targetedIntent.setComponent(mRCStack.peek().mReceiverComponent);
  29. // trap the current broadcast
  30. // 终止系统广播
  31. abortBroadcast();
  32. //Log.v(TAG, " Sending intent" + targetedIntent);
  33. //手动发送该广播至目标对象去处理,该广播不再是系统发送的了
  34. context.sendBroadcast(targetedIntent, null);
  35. }
  36. //假设栈为空,那么所有定义在AndroidManifest.xml的监听MEDIA_BUTTON的广播都会处理,
  37. //在此过程中如果有任何应用程注册了registerMediaButton 该广播也会立即终止
  38. }
  39. }
  40. }
  41. }
    private class MediaButtonBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //获得action ,系统MEDIA_BUTTON广播来了
            String action = intent.getAction();
            //action不正确 直接返回
            if (!Intent.ACTION_MEDIA_BUTTON.equals(action)) {
                return;
            }
          //获得KeyEvent对象
            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            if (event != null) {
                // if in a call or ringing, do not break the current phone app behavior
                // TODO modify this to let the phone app specifically get the RC focus
                //      add modify the phone app to take advantage of the new API
                //来电或通话中,不做处理直接返回
                if ((getMode() == AudioSystem.MODE_IN_CALL) ||(getMode() == AudioSystem.MODE_RINGTONE)) {
                    return;
                }
                synchronized(mRCStack) {
                    //栈不为空
                    if (!mRCStack.empty()) {
                        // create a new intent specifically aimed at the current registered listener
                        //构造一个Intent对象 ,并且赋予Action和KeyEvent
                        Intent targetedIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
                        targetedIntent.putExtras(intent.getExtras());
                        //指定该处理Intent的对象为栈顶ComponentName对象的广播类
                            targetedIntent.setComponent(mRCStack.peek().mReceiverComponent);
                        // trap the current broadcast
                        // 终止系统广播
                             abortBroadcast();
                        //Log.v(TAG, " Sending intent" + targetedIntent);
                        //手动发送该广播至目标对象去处理,该广播不再是系统发送的了
                            context.sendBroadcast(targetedIntent, null);
                    }
                    //假设栈为空,那么所有定义在AndroidManifest.xml的监听MEDIA_BUTTON的广播都会处理,
                    //在此过程中如果有任何应用程注册了registerMediaButton 该广播也会立即终止
                }
            }
        }
    }

总结一下MEDIA_BUTTON广播:

AudioManager也就是AudioService服务端对象内部会利用一个栈来管理所有ComponentName对象,所有对象有且仅有一个,

新注册的ComponentName总是会位于栈顶。

当系统发送MEDIA_BUTTON,系统MediaButtonBroadcastReceiver 监听到系统广播,它会做如下处理:

1、 如果栈为空,则所有注册了该Action的广播都会接受到,因为它是由系统发送的。
2、 如果栈不为空,那么只有栈顶的那个广播能接受到MEDIA_BUTTON的广播,手动发送了MEDIA_BUTTON

广播,并且指定了目标对象(栈顶对象)去处理该MEDIA_BUTTON 。

下面分析一下KeyEvent对象里的KeyCode按键,可能的按键码有:

1、KeyEvent.KEYCODE_MEDIA_NEXT
2、KeyEvent.KEYCODE_HEADSETHOOK
3、KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE(已废除,等同于KEYCODE_HEADSETHOOK)
4、KeyEvent.KEYCODE_MEDIA_PREVIOUS
5、KeyEvent.KEYCODE_MEDIA_STOP

PS : 在我的真机测试中,按下MEDIA_BUTTON只有KEYCODE_HEADSETHOOK可以打印出来了。

下面给出一个小DEMO检验一下我们之前所做的一切,看看MEDIA_BUTTON是如何处理分发广播的。

编写两个MediaButtonReceiver类用来监听MEDIA_BUTTON广播:

1 、China_MBReceiver.java

[java] view plaincopyprint?

  1. package com.qin.mediabutton;
  2. import android.content.BroadcastReceiver;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.util.Log;
  6. import android.view.KeyEvent;
  7. public class China_MBReceiver extends BroadcastReceiver {
  8. private static String TAG = "China_MBReceiver" ;
  9. @Override
  10. public void onReceive(Context context, Intent intent) {
  11. //获得Action
  12. String intentAction = intent.getAction() ;
  13. //获得KeyEvent对象
  14. KeyEvent keyEvent = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
  15. Log.i(TAG, "Action ---->"+intentAction + " KeyEvent----->"+keyEvent.toString());
  16. if(Intent.ACTION_MEDIA_BUTTON.equals(intentAction)){
  17. //获得按键字节码
  18. int keyCode = keyEvent.getKeyCode() ;
  19. //按下 / 松开 按钮
  20. int keyAction = keyEvent.getAction() ;
  21. //获得事件的时间
  22. long downtime = keyEvent.getEventTime();
  23. //获取按键码 keyCode
  24. StringBuilder sb = new StringBuilder();
  25. //这些都是可能的按键码 , 打印出来用户按下的键
  26. if(KeyEvent.KEYCODE_MEDIA_NEXT == keyCode){
  27. sb.append("KEYCODE_MEDIA_NEXT");
  28. }
  29. //说明:当我们按下MEDIA_BUTTON中间按钮时,实际出发的是 KEYCODE_HEADSETHOOK 而不是 KEYCODE_MEDIA_PLAY_PAUSE
  30. if(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ==keyCode){
  31. sb.append("KEYCODE_MEDIA_PLAY_PAUSE");
  32. }
  33. if(KeyEvent.KEYCODE_HEADSETHOOK == keyCode){
  34. sb.append("KEYCODE_HEADSETHOOK");
  35. }
  36. if(KeyEvent.KEYCODE_MEDIA_PREVIOUS ==keyCode){
  37. sb.append("KEYCODE_MEDIA_PREVIOUS");
  38. }
  39. if(KeyEvent.KEYCODE_MEDIA_STOP ==keyCode){
  40. sb.append("KEYCODE_MEDIA_STOP");
  41. }
  42. //输出点击的按键码
  43. Log.i(TAG, sb.toString());
  44. }
  45. }
  46. }
package com.qin.mediabutton;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.KeyEvent;

public class China_MBReceiver extends BroadcastReceiver  {

    private static String TAG = "China_MBReceiver" ;
    @Override
    public void onReceive(Context context, Intent intent) {
        //获得Action
        String intentAction = intent.getAction() ;
        //获得KeyEvent对象
        KeyEvent keyEvent = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);

        Log.i(TAG, "Action ---->"+intentAction + "  KeyEvent----->"+keyEvent.toString());

        if(Intent.ACTION_MEDIA_BUTTON.equals(intentAction)){
            //获得按键字节码
            int keyCode = keyEvent.getKeyCode() ;
            //按下 / 松开 按钮
            int keyAction = keyEvent.getAction() ;
            //获得事件的时间
            long downtime = keyEvent.getEventTime();

            //获取按键码 keyCode
            StringBuilder sb = new StringBuilder();
            //这些都是可能的按键码 , 打印出来用户按下的键
            if(KeyEvent.KEYCODE_MEDIA_NEXT == keyCode){
                sb.append("KEYCODE_MEDIA_NEXT");
            }
            //说明:当我们按下MEDIA_BUTTON中间按钮时,实际出发的是 KEYCODE_HEADSETHOOK 而不是 KEYCODE_MEDIA_PLAY_PAUSE
            if(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ==keyCode){
                sb.append("KEYCODE_MEDIA_PLAY_PAUSE");
            }
            if(KeyEvent.KEYCODE_HEADSETHOOK == keyCode){
                sb.append("KEYCODE_HEADSETHOOK");
            }
            if(KeyEvent.KEYCODE_MEDIA_PREVIOUS ==keyCode){
                sb.append("KEYCODE_MEDIA_PREVIOUS");
            }
            if(KeyEvent.KEYCODE_MEDIA_STOP ==keyCode){
                sb.append("KEYCODE_MEDIA_STOP");
            }
            //输出点击的按键码
            Log.i(TAG, sb.toString());

        }

    }

}

2 、England_MBReceiver.java同于China_MBRreceiver ,打印Log TAG= "England_MBReceiver"

3、在AndroidManifest.xml文件定义:

[java] view plaincopyprint?

  1. ".China_MBReceiver">
  2. "android.intent.action.MEDIA_BUTTON">
  3. ".Enaland_MBReceiver">
  4. "android.intent.action.MEDIA_BUTTON">
 

        

4、MainActivity .java 我们通过手动构造一个MEDIA_BUTTON广播去查看我们的MediaButtonReceiver类的打印信息。

[java] view plaincopyprint?

  1. package com.qin.mediabutton;
  2. import android.app.Activity;
  3. import android.content.ComponentName;
  4. import android.content.Context;
  5. import android.content.Intent;
  6. import android.media.AudioManager;
  7. import android.os.Bundle;
  8. import android.view.KeyEvent;
  9. public class MainActivity extends Activity {
  10. /** Called when the activity is first created. */
  11. @Override
  12. public void onCreate(Bundle savedInstanceState) {
  13. super.onCreate(savedInstanceState);
  14. setContentView(R.layout.main);
  15. //由于在模拟器上测试,我们手动发送一个MEDIA_BUTTON的广播,有真机更好处理了
  16. Intent mbIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
  17. //构造一个KeyEvent对象
  18. KeyEvent keyEvent = new KeyEvent (KeyEvent.ACTION_DOWN,KeyEvent.KEYCODE_HEADSETHOOK) ;
  19. //作为附加值添加至mbIntent对象中
  20. mbIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
  21. //此时China_MBReceiver和England_MBReceiver都会接收到该广播
  22. sendBroadcast(mbIntent);
  23. AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
  24. //AudioManager注册一个MediaButton对象
  25. ComponentName chinaCN = new ComponentName(getPackageName(),China_MBReceiver.class.getName());
  26. //只有China_MBReceiver能够接收到了,它是出于栈顶的。
  27. //不过,在模拟上检测不到这个效果,因为这个广播是我们发送的,流程不是我们在上面介绍的。
  28. mAudioManager.registerMediaButtonEventReceiver(chinaCN);
  29. //sendBroadcast(mbIntent,null);
  30. }
  31. //当一个Activity/Service死去时,我们需要取消这个MediaoButtonReceiver的注册,如下
  32. protected void onDestroy(){
  33. super.onDestroy() ;
  34. AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
  35. ComponentName chinaCN = new ComponentName(getPackageName(),China_MBReceiver.class.getName());
  36. //取消注册
  37. mAudioManager.unregisterMediaButtonEventReceiver(chinaCN);
  38. }
  39. }
package com.qin.mediabutton;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.view.KeyEvent;

public class MainActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //由于在模拟器上测试,我们手动发送一个MEDIA_BUTTON的广播,有真机更好处理了
        Intent mbIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        //构造一个KeyEvent对象
        KeyEvent keyEvent = new KeyEvent (KeyEvent.ACTION_DOWN,KeyEvent.KEYCODE_HEADSETHOOK) ;
        //作为附加值添加至mbIntent对象中
        mbIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);

        //此时China_MBReceiver和England_MBReceiver都会接收到该广播
        sendBroadcast(mbIntent);

        AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
        //AudioManager注册一个MediaButton对象
        ComponentName chinaCN = new ComponentName(getPackageName(),China_MBReceiver.class.getName());
        //只有China_MBReceiver能够接收到了,它是出于栈顶的。
        //不过,在模拟上检测不到这个效果,因为这个广播是我们发送的,流程不是我们在上面介绍的。
        mAudioManager.registerMediaButtonEventReceiver(chinaCN);
       //sendBroadcast(mbIntent,null);
    }
   //当一个Activity/Service死去时,我们需要取消这个MediaoButtonReceiver的注册,如下
    protected void onDestroy(){
        super.onDestroy() ;
        AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
        ComponentName chinaCN = new ComponentName(getPackageName(),China_MBReceiver.class.getName());
        //取消注册
        mAudioManager.unregisterMediaButtonEventReceiver(chinaCN);
    }
}

值得注意的一点时,当我们为一个应用程序注册了MediaoButtonReceiver时,在程序离开时,我们需要取消

MediaoButtonReceiver的注册,在onDestroy()调用unregisterMediaButtonEventReceiver()方法就OK,这样应用程序之间

的交互就更具逻辑性了。

Similar Posts:

  • Android 蓝牙开发(整理大全)

    Android蓝牙开发 鉴于国内Android蓝牙开发的例子很少,以及蓝牙开发也比较少用到,所以找的资料不是很全. (一): 由于Android蓝牙的通信都需要用到UUID,如果由手机发起搜索,当搜索到电脑的蓝牙时,能够得到蓝牙的地址(address),但通信时需要得到BluetoothSocket,而BluetoothSocket则需要电脑蓝牙的UUID,请问这个是怎么样得到的呢? 在蓝牙中,每个服务和服务属性都唯一地由"全球唯一标识符" (UUID)来校验.正如它的名字所暗示的,每

  • Android 属性动画浅谈(一)插值器和估值器

    Android 属性动画浅谈(一)插值器和估值器 属性动画是API11新加入的特性,和View动画不同,它对作用对象进行了扩展,属性动画可以对任何对象动画,甚至还可以没有对象.除了作用对象进行了扩展以外,属性动画的效果也得到了假期,不再像View动画那样只能支持4中简单的变换.属性动画中有ValueAnimator.ObjectAnimator和AnimatorSet等概念.通过它们可以实现炫彩的的效果! 属性动画中插值器(Interpolator)和估值器(TypeEvaluator)很重要,

  • Android蓝牙开发简介

    Android蓝牙系统 蓝牙是一种支持设备短距离通信(一般10m内)的无线电技术,可以在众多设备之间进行无线信息交换. Android系统中的蓝牙模块 Android包含了对蓝牙网络协议栈的支持,使蓝牙设备能够无线连接其他蓝牙设备以便交换数据. 通过使用蓝牙API,一个Android应用程序能够实现如下功能: - 扫描其他蓝牙设备 - 查询本地蓝牙适配器用于配对蓝牙设备 - 建立RFCOMM信道 - 通过服务发现连接其他设备 - 数据通信 - 管理多个连接 Android平台中蓝牙系统从上到下主

  • 听一名普通android应用开发人员谈:怎样成为一名Android开发者

    Chris(克里斯)是一位来自波兰的Android应用开发者,最为一名非著名的开发者他开发的应用在Android Market上免费提供下载,并通过广告获得收入,最近他在自己的博客上面分享了从事Android开发带来的收入情况,并通过自己的经历给予 Android开发入门者非常忠实的忠告.作为国内从事Android开发的同行,APP虎第一时间发现并翻译了他的文章,以与广大Android开发者和即将进入Android开发的人们分享. 很多人认为从事Android开发是一件赔本的买卖. 他们说,如果

  • Android Studio NDK开发浅谈

    环境: Android Studio 1.1.0 NDK-r10d 1.新建项目--->包名:com.mxl.az.ndk 新建包含native方法的类:JniOperation.class public class JniOperation { public static native String getString(); public native int add(int a, int b); } 2.然后使用javah命令生成.h文件 打开"小黑框",进入项目目录的...

  • Android开发浅谈-04-Activity信息传递

    Activity从某种程度上说, 是Android大多数程序的骨架子. 一般很少会有程序只有一个Activity, 因此很多时候, Activity之间的通信和变量传递是非常重要的. Android SDK本身是提供了四种跨进程通讯方式的, 对应了四种应用程序组件: Activity, Content Provider, Broadcast和Service. 在Ophone社区也有对这几种方式的简单说明: Activity可以跨进程调用其他应用程序的Activity(自身应用程序当然也可以);

  • Android JNI开发之使用Lame将录音转MP3

    写在前面 开发环境 前言 Lame 源码下载 写在前面 由于在写之前在网上搜了搜,发现GIthub上已经有AndroidStudio版使用Lame转Mp3的了,所以这篇博客写的例子是Eclipse版本,所以如果是AS的请移步到别人之前已经写好的Github地址:https://github.com/GavinCT/AndroidMP3Recorder (未亲测过) 最终实现效果: 开发环境 IDE版本:Eclipse 物理机版本:Win7旗舰版(64位) 前言 在实际开发中,录音出现的频率还是很

  • android 蓝牙开发教程

    蓝牙是啥我就不再说了,因为我肯定解释不清楚蓝牙是啥,有兴趣请移步至百度百科. 在开始前我们需要准备好一部手机而不是模拟器,且手机已经打开调试模式并连接到电脑上.文中的本地设备均指我们自己的手机,而远程设备则是指其他的设备(电脑.其他手机或者其他). 通常情况下,我们对蓝牙的操作主要有:开启和关闭蓝牙.搜索周边设备.能被周边设备所发现.获取配对设备.蓝牙设备间的数据传输. 1.打开蓝牙(当然首先要确保你的手机是有蓝牙设备的) 蓝牙设备主要分为两部分,一部分为本地设备,另一部分为远程设备. Blue

  • android蓝牙开发浅析2——startDiscovery

    对于BluetoothAdapter的startDiscovery()方法应该从以下几点理解: 一.startDiscovery方法有效的前提是本机必须打开蓝牙适配器,如果本机没有打开蓝牙适配器(可以调用BluetoothAdapter的enable方法打开蓝牙设备),android系统是什么都 不做的,这时连搜索开始的广播系统都没有发送,遑论其他的操作. 二.startDiscovery方法不是一个方法,而是一个异步方法,顾名思义,就是说此方法开启了一个子线程,工作是在子线程中进行的. 三.s

  • 新人浅谈__(数据库的设计__数据库模型图,数据库E-R图,三大范式)

    >>>> 为什么需要规范的数据库设计 在实际的项目开发中,如果系统的数据存储量较大,设计的表比较多,表和表之间的关系比较复杂,就需要首先考虑规范的数据库设计,然后进行创建库,创建表的工作. 如果设计不当,会存在数据操作异常,修改复杂,数据冗余等问题,程序性能会受到影响,通过进行规范化的数据库设计,可以消除不必要的数据冗余,获得合理的数据库设计,提高项目的应用性能. >>>>设计数据库的步骤 1.收集信息 需要了解数据库需要存储哪些信息(数据),实现哪些功能.

Tags: