轉載請註明出處:http://blog.csdn.net/vnanyesheshou/article/details/70256004
本文已授權微信公衆號 fanfan程序媛 獨家發佈 掃一掃文章底部的二維碼或在微信搜索 fanfan程序媛 即可關注
Android藍牙功能(傳統藍牙、ble、hid)這三方面功能之前的博客都已經寫了。現在接着瞭解藍牙OPP傳輸文件相關功能。Android手機使用中,經常會用到通過藍牙分享文件給附近的朋友。那麼具體是如何實現的,大部分朋友都不是很清楚。看一下源碼是如何實現該功能的。
1 BluetoothOppLauncherActivity |
Android手機點擊某文件進行藍牙分享的時候,會跳轉到系統自帶應用Bluetooth中。
具體文件:packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
看一下BluetoothOppLauncherActivity是如何處理分享文件請求的。
if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) {
//Check if Bluetooth is available in the beginning instead of at the end
if (!isBluetoothAllowed()) {
Intent in = new Intent(this, BluetoothOppBtErrorActivity.class);
in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
in.putExtra("title", this.getString(R.string.airplane_error_title));
in.putExtra("content", this.getString(R.string.airplane_error_msg));
startActivity(in);
finish();
return;
}
//..........下面接着說。
}
BluetoothOppLauncherActivity並沒有界面(沒有setContentView),只是一箇中轉站,它根據當前藍牙等相關狀態進行跳轉。Intent.ACTION_SEND和Intent.ACTION_SEND_MULTIPLE的區別是前者表示單個文件,後者表示多個文件。這裏只研究下分享單個文件,分享單個文件懂了,多個文件道理類似。
其中isBluetoothAllowed()函數會先判斷飛行模式是否開啓,如果沒有開啓則返回true。如果開啓,則進行下一步判斷飛行模式是否重要,如果不重要則返回true(說明藍牙可以使用)。如果重要則繼續分析飛行模式下是否可以打開藍牙,可以打開藍牙則返回true,否則返回false。總的來說該函數就是判斷當前藍牙是否允許使用。不允許使用藍牙則跳轉到BluetoothOppBtErrorActivity。
接着向下:
if (action.equals(Intent.ACTION_SEND)) { //單個文件
final String type = intent.getType();
final Uri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM);
CharSequence extra_text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (stream != null && type != null) { //分享文件
Thread t = new Thread(new Runnable() {
public void run() {
BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this)
.saveSendingFileInfo(type,stream.toString(), false);
launchDevicePicker();
finish();
}
});
t.start();
return;
} else if (extra_text != null && type != null) { //分享text字符串,沒有文件
final Uri fileUri = creatFileForSharedContent(this, extra_text); //創建文件,將內容寫入文件
if (fileUri != null) {
Thread t = new Thread(new Runnable() {
public void run() {
BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this)
.saveSendingFileInfo(type,fileUri.toString(), false);
launchDevicePicker();
finish();
}
});
t.start();
return;
}
//.........
}
使用過Android系統分享的應該知道,其支持文件(圖片、視頻等)、字符串。而這裏會對文件、字符串進行區分處理,字符串則先創建文件然後在進行分享。
launchDevicePicker()函數中先判斷藍牙是否開啓。
如果藍牙沒有開啓則跳轉到BluetoothOppBtEnableActivity顯示dialog(詢問是否開啓藍牙),點擊取消則則退出,點擊打開則打開藍牙並跳到BluetoothOppBtEnablingActivity(該activity主要顯示一個progress dialog)。當藍牙打開,則BluetoothOppBtEnablingActivity 界面finish。BluetoothOppReceiver廣播接收者接收到藍牙開啓,跳轉到DevicePickerActivity界面(系統Settings應用)。
如果藍牙已開啓,則直接跳轉到跳轉到DevicePickerActivity界面(系統Settings應用)。
launchDevicePicker()下的跳轉代碼:
//ACTION_LAUNCH="android.bluetooth.devicepicker.action.LAUNCH"
Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
BluetoothDevicePicker.FILTER_TYPE_TRANSFER);
in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE,
Constants.THIS_PACKAGE_NAME);
in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS,
BluetoothOppReceiver.class.getName());
startActivity(in1);
系統Settings應用中AndroidManifest.xml中發現對應action的DevicePickerActivity,所以該跳轉會跳轉到系統Settings應用中的DevicePickerActivity中。
<activity android:name=".bluetooth.DevicePickerActivity"
android:uiOptions="splitActionBarWhenNarrow"
android:theme="@android:style/Theme.Holo.DialogWhenLarge"
android:label="@string/device_picker"
android:clearTaskOnLaunch="true">
<intent-filter>
<action android:name="android.bluetooth.devicepicker.action.LAUNCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
2 DevicePicker |
DevicePickerActivity中代碼很簡單,只是設置了佈局。
setContentView(R.layout.bluetooth_device_picker);
bluetooth_device_picker.xml中有一個fragment指向DevicePickerFragment,也就是主要的處理在DevicePickerFragment中。
DevicePickerFragment界面會顯示出配對、掃描到的藍牙列表。可以點擊一個設備進行分享文件。
void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
mLocalAdapter.stopScanning(); //停止掃描
LocalBluetoothPreferences.persistSelectedDeviceInPicker(
getActivity(), mSelectedDevice.getAddress());
if ((btPreference.getCachedDevice().getBondState() ==
BluetoothDevice.BOND_BONDED) || !mNeedAuth) {
sendDevicePickedIntent(mSelectedDevice);
finish();
} else {
super.onDevicePreferenceClick(btPreference);
}
}
點擊設備,會判斷是否是綁定狀態,或者mNeedAuth爲false。mNeedAuth是通過intent傳過來的值爲false。所以滿足條件。
接着看sendDevicePickedIntent()。該函數就是發了一個廣播。
private void sendDevicePickedIntent(BluetoothDevice device) {
//"android.bluetooth.devicepicker.action.DEVICE_SELECTED"
Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
if (mLaunchPackage != null && mLaunchClass != null) {
intent.setClassName(mLaunchPackage, mLaunchClass);
}
getActivity().sendBroadcast(intent);
}
3 BluetoothOppReceiver |
查看系統應用Bluetooth中的BluetoothOppReceiver類中對此廣播進行了處理。但是Bluetooth中的AndroidManifest.xml中該廣播接收者的註冊並沒有添加此action。但是卻可以接收此廣播。原因應該是該廣播發送時攜帶了包名、類名。
<receiver
android:process="@string/process"
android:exported="true"
android:name=".opp.BluetoothOppReceiver"
android:enabled="@bool/profile_supported_opp">
<intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<!--action android:name="android.intent.action.BOOT_COMPLETED" /-->
<action android:name="android.btopp.intent.action.OPEN_RECEIVED_FILES" />
</intent-filter>
</receiver>
BluetoothOppReceiver收到此廣播後的主要處理代碼如下,將此條記錄添加到數據庫。
// Insert transfer session record to database
mOppManager.startTransfer(remoteDevice);
BluetoothOppManager對象調用startTransfer方法。在startTransfer方法中創建一個InsertShareInfoThread線程並開始運行。
InsertShareInfoThread線程中區分分享的是一個文件還是多個文件。我們這裏只看下處理單個文件insertSingleShare()函數。
if (mIsMultiple) {//多個文件
insertMultipleShare();
} else { //單個文件
insertSingleShare();
}
private void insertSingleShare() {
ContentValues values = new ContentValues();
values.put(BluetoothShare.URI, mUri);
values.put(BluetoothShare.MIMETYPE, mTypeOfSingleFile);
values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
if (mIsHandoverInitiated) {
values.put(BluetoothShare.USER_CONFIRMATION,
BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
}
final Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI,
values);
}
由mContext.getContentResolver().insert()可知其有對應的provider。BluetoothOppProvider繼承了ContextProvider。查看BluetoothOppProvider中的insert方法。
public Uri insert(Uri uri, ContentValues values) {
.....
if (rowID != -1) {
context.startService(new Intent(context, BluetoothOppService.class));
ret = Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID);
context.getContentResolver().notifyChange(uri, null);
}
由上可知通過藍牙分享的時候會start BluetoothOppService。
4 BluetoothOppService |
在BluetoothOppService中會監聽數據庫字段(BluetoothShare.CONTENT_URI)的變化,調用updateFromProvider()函數進行處理。onCreate()和onStartCommand()函數都會調用updateFromProvider()。
updateFromProvider() ->創建線程UpdateThread -> insertShare()。
private void insertShare(Cursor cursor, int arrayPos) {
if (info.isReadyToStart()) {
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { //向外分享、發送
/* 檢查文件是否存在 */
}
}
if (mBatchs.size() == 0) {
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {//向外分享、發送
mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
} else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) { //接收
mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
mServerSession);
}
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
mTransfer.start();
} else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
&& mServerTransfer != null) {
mServerTransfer.start();
}
}
//........
}
5 BluetoothOppTransfer |
這裏只說向外發送、分享。接着看BluetoothOppTransfer。
public void start() {
//檢查藍牙是否打開,保證安全
if (!mAdapter.isEnabled()) {
return;
}
if (mHandlerThread == null) {
//......
if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
/* for outbound transfer, we do connect first */
startConnectSession();
}
//....
}
}
startConnectSession()函數中開始向遠端設備進行連接,該函數中主要就是創建SocketConnectThread線程,用來連接其他設備。
SocketConnectThread線程主要代碼:
try { //創建BluetoothSocket
btSocket = device.createInsecureRfcommSocketToServiceRecord(BluetoothUuid.ObexObjectPush.getUuid());
} catch (IOException e1) {//....
}
try {
btSocket.connect(); //;連接設備
BluetoothOppRfcommTransport transport;
transport = new BluetoothOppRfcommTransport(btSocket);
BluetoothOppPreference.getInstance(mContext).setName(device, device.getName());
mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
} catch (IOException e) {//....
}
這裏先創建BluetoothSocket,然後通過BluetoothSocket進行連接。
連接成功後,startObexSession()->new BluetoothOppObexClientSession ->BluetoothOppObexClientSession .start()
6 BluetoothOppObexClientSession |
BluetoothOppObexClientSession類說明該設備作爲obex client,向server發送文件。該類中主要功能:obex連接、發送分享文件的信息,發送數據等。
start() -> 創建ClientThread線程並運行 -> connect()。
在connect()函數中,通過mTransport1(BluetoothOppRfcommTransport類型,該類型中主要包含之前創建的BluetoothSocket)對象,創建client session,連接遠端設備。
private void connect(int numShares) {
try {//創建obex client
mCs = new ClientSession(mTransport1);
mConnected = true;
} catch (IOException e1) {
}
if (mConnected) {
mConnected = false;
HeaderSet hs = new HeaderSet(); //obex 連接攜帶信息
hs.setHeader(HeaderSet.COUNT, (long) numShares);//文件數量
synchronized (this) {
mWaitingForRemote = true;
}
try { //obex連接
mCs.connect(hs);
mConnected = true;
} catch (IOException e) {
}
}
//.....
}
obex連接成功後,調用doSend(),該函數中先檢查下文件是否存在,然後查看連接狀態,連接狀態下並且存在文件則sendFile才真正的開始發送文件。之會將相應的狀態發送到BluetoothOppTransfer中。
private void doSend() {
int status = BluetoothShare.STATUS_SUCCESS;
while (mFileInfo == null) { //檢查文件是否存在
try {
Thread.sleep(50);
} catch (InterruptedException e) {
status = BluetoothShare.STATUS_CANCELED;
}
}
//檢查連接狀態
if (!mConnected) {
status = BluetoothShare.STATUS_CONNECTION_ERROR;
}
if (status == BluetoothShare.STATUS_SUCCESS) {
/* 發送文件*/
if (mFileInfo.mFileName != null) {
status = sendFile(mFileInfo);
} else {
status = mFileInfo.mStatus;
}
waitingForShare = true;
} else {
Constants.updateShareStatus(mContext1, mInfo.mId, status);
}
//發送此次操作是否成功等信息。
}
真正的發送文件是在sendFile()函數中。不過該函數太長就不全貼出來了,只說一下重要的地方。
1 發送文件信息
HeaderSet request = new HeaderSet();
request.setHeader(HeaderSet.NAME, fileInfo.mFileName); //文件名
request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype); //文件類型
request.setHeader(HeaderSet.LENGTH, fileInfo.mLength); //文件大小
//通過obex發送傳遞文件請求
putOperation = (ClientOperation)mCs.put(request);
//putOperation類型爲ClientOperation,具體java.obex包下的類沒有向外透漏,不太清楚是具體怎麼回事。
2 獲取obex層輸入輸出流
//獲取輸入輸出流。
outputStream = putOperation.openOutputStream();
inputStream = putOperation.openInputStream();
3 發送第一個包
//從文件中讀取內容
BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);
readLength = readFully(a, buffer, outputBufferSize);
//先向遠程設備發送第一個包 該操作會阻塞等待遠端設備的接收讀取。
outputStream.write(buffer, 0, readLength);
position += readLength;
如果文件太小,一個包就已經發送完,則將輸出流關閉。outputStream.close();
4 查看回應
接着查看遠端設備的迴應,是否接受。
/* check remote accept or reject */
responseCode = putOperation.getResponseCode();
if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
|| responseCode == ResponseCodes.OBEX_HTTP_OK) {
//接收
okToProceed = true;
updateValues = new ContentValues();
updateValues.put(BluetoothShare.CURRENT_BYTES, position);
mContext1.getContentResolver().update(contentUri, updateValues, null,
null);
} else {//拒絕接收
Log.i(TAG, "Remote reject, Response code is " + responseCode);
}
5 判斷髮送數據
接着循環判斷、從文件讀取數據、發送數據。
while (!mInterrupted && okToProceed && (position != fileInfo.mLength)) {
readLength = a.read(buffer, 0, outputBufferSize);
outputStream.write(buffer, 0, readLength);
/* check remote abort */
responseCode = putOperation.getResponseCode();
if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
&& responseCode != ResponseCodes.OBEX_HTTP_OK) {
okToProceed = false;
} else {
position += readLength;
//更行進度
updateValues = new ContentValues();
updateValues.put(BluetoothShare.CURRENT_BYTES, position);
mContext1.getContentResolver().update(contentUri, updateValues,
null, null);
}
}
在之後就是一些狀態的處理了。到此通過藍牙分享文件到流程基本上過了一遍,其中還有許多狀態、進度等相關功還沒能研究透徹,之後再繼續研究。
歡迎掃一掃關注我的微信公衆號,定期推送優質技術文章: