転載元: http://blog.csdn.net/vnanyesheshou/article/details/70256004
Android の Bluetooth 機能の 3 つの側面 (従来の Bluetooth、ble、hid ) については、以前のブログで書きました。ここでは、 Bluetooth OPP ファイル転送に関連する機能について学びましょう。Android スマートフォンを使用している場合、Bluetooth 経由で近くの友人とファイルを共有することが必要になることがよくあります。では、それがどのように正確に実装されるのか、ほとんどの友人はそれについてあまり明確ではありません。ソース コードがこの関数をどのように実装しているかを見てください。
1 BluetoothOppLauncherアクティビティ
1 BluetoothOppLauncherアクティビティ |
Android スマートフォンでBluetooth 経由で共有するファイルをクリックすると、システムの組み込み Bluetooth アプリケーションにジャンプします。
特定のファイル: package/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 なし) がなく、現在の Bluetooth およびその他の関連ステータスに応じてジャンプする単なる転送ステーションです。Intent.ACTION_SEND と Intent.ACTION_SEND_MULTIPLE の違いは、前者は単一のファイルを表し、後者は複数のファイルを表すことです。ここでは 1 つのファイルの共有についてのみ説明します。1 つのファイルの共有については理解できたと思います。原理は複数のファイルについても同様です。
isBluetoothAllowed ()関数は、まずフライト モードがオンかどうかを判断し、オンでない場合は true を返します。オンになっている場合は次のステップに進み、機内モードが重要かどうかを判断し、重要でない場合は true (Bluetooth が使用可能であることを示す) を返します。重要な場合は、機内モードで Bluetooth をオンにできるかどうかの分析を続けます。Bluetooth をオンにできる場合は true を返し、そうでない場合は false を返します。一般に、この機能は、現在の Bluetooth の使用が許可されているかどうかを判断することです。Bluetooth の使用が許可されていない場合は、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 システム共有を使用したことのある人は、Android システム共有がファイル (写真、ビデオなど) と文字列をサポートしていることを知っているはずです。ここではファイルと文字列を区別し、文字列の場合はファイルを作成してから共有します。
launchDevicePicker() 関数では、まずBluetooth がオンになっているかどうかを判断します。
Bluetooth がオンになっていない場合は、BluetoothOppBtEnableActivity にジャンプしてダイアログを表示します (Bluetooth をオンにするかどうかを尋ねます)。[キャンセル] をクリックして終了します。[開く] をクリックして Bluetooth をオンにし、BluetoothOppBtEnablingActivity にジャンプします (このアクティビティは主に進行状況ダイアログを表示します)。Bluetooth がオンになると、BluetoothOppBtEnablingActivity インターフェイスが終了します。BluetoothOppReceiver ブロードキャスト レシーバーは、Bluetooth がオンになったことを受信し、DevicePickerActivity インターフェイス (システム設定アプリケーション) にジャンプします。
Bluetooth がオンになっている場合は、DevicePickerActivityインターフェイス (システム設定アプリケーション) に直接ジャンプします。
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);
アクションに対応する DevicePickerActivity はシステム設定アプリケーションの AndroidManifest.xml にあるため、システム設定アプリケーションの 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 デバイスピッカー
2 デバイスピッカー |
DevicePickerActivity のコードは非常に単純で、レイアウトを設定するだけです。
setContentView(R.layout.bluetooth_device_picker);
bluetooth_device_picker.xml には DevicePickerFragment を指すフラグメントがあり、主な処理は DevicePickerFragment にあります。
DevicePickerFragmentインターフェイスには、ペアリングおよびスキャンされた Bluetooth リストが表示されます。デバイスをクリックしてファイルを共有できます。
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 によって渡される値は 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
3 BluetoothOppReceiver |
このブロードキャストがシステム アプリケーション Bluetooth のBluetoothOppReceiverクラスで処理されていることを確認します。ただし、Bluetooth の AndroidManifest.xml へのブロードキャスト レシーバーの登録では、このアクションは追加されません。ただし、この放送は受信できます。その理由は、ブロードキャストが送信時にパッケージ名とクラス名を運ぶためです。
<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 スレッドは、1 つのファイルが共有されているか複数のファイルが共有されているかを区別します。ここでは、単一のファイルを処理する 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() から、対応するプロバイダーがあることがわかります。BluetoothOppProvider はContextProvider を継承します。BluetoothOppProvider の挿入メソッドを確認してください。
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);
}
上記から、Bluetooth 経由で共有するときに BluetoothOppService が開始されることがわかります。
4 BluetoothOppサービス
4 BluetoothOppサービス |
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 BluetoothOpp転送
5 BluetoothOpp転送 |
ここでは送信と共有についてのみ説明します。次に 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 {
if (mIsInterrupted) {
Log.e(TAG, "btSocket connect interrupted ");
markConnectionFailed(mBtSocket);
return;
} else {
mBtSocket = mDevice.createInsecureL2capSocket(mL2cChannel);
}
} catch (IOException e1) {
Log.e(TAG, "L2cap socket create error", e1);
connectRfcommSocket();
return;
}
try {
mBtSocket.connect();
if (D) {
Log.v(TAG, "L2cap socket connection attempt took " + (System.currentTimeMillis()
- mTimestamp) + " ms");
}
BluetoothObexTransport transport;
transport = new BluetoothObexTransport(mBtSocket);
BluetoothOppPreference.getInstance(mContext).setName(mDevice, mDevice.getName());
if (V) {
Log.v(TAG, "Send transport message " + transport.toString());
}
mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget();
}
} catch (IOException e) {
\...
}
ここでは、まず BluetoothSocket を作成し、次に BluetoothSocket を通じて接続します。
ソケットは L2capSocket である必要があります。
Transport = new BluetoothObexTransport(mBtSocket); は後で使用します。
case TRANSPORT_CONNECTED:
/*
* RFCOMM connected is for outbound share only! Create
* BluetoothOppObexClientSession and start it
*/
if (V) {
Log.v(TAG, "Transfer receive TRANSPORT_CONNECTED msg");
}
mTransport = (ObexTransport) msg.obj;
startObexSession();
break;
连接成功后,通过Handler调用startObexSession()->new BluetoothOppObexClientSession ->BluetoothOppObexClientSession .start()
6 BluetoothOppObexClientSession
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);
}
}
outputStream是PrivateOutputStream对象
PrivateOutputStream.java
public synchronized void write(byte[] buffer, int offset, int count) throws IOException {
int offset1 = offset;
int remainLength = count;
if (buffer == null) {
throw new IOException("buffer is null");
}
if ((offset | count) < 0 || count > buffer.length - offset) {
throw new IndexOutOfBoundsException("index outof bound");
}
ensureOpen();
mParent.ensureNotDone();
while ((mArray.size() + remainLength) >= mMaxPacketSize) {
int bufferLeft = mMaxPacketSize - mArray.size();
mArray.write(buffer, offset1, bufferLeft);
offset1 += bufferLeft;
remainLength -= bufferLeft;
mParent.continueOperation(true, false);
}
if (remainLength > 0) {
mArray.write(buffer, offset1, remainLength);
}
}
public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream)
throws IOException {
// One path to the first put operation - the other one does not need to
// handle SRM, as all will fit into one packet.
if (mGetOperation) {
...
} else {
// PUT operation
if ((!inStream) && (!mOperationDone)) {
// to deal with outputstream in put operation
if (mReplyHeader.responseCode == -1) {
mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE;
}
sendRequest(ObexHelper.OBEX_OPCODE_PUT);
return true;
} else if ((inStream) && (!mOperationDone)) {
// How to deal with inputstream in put operation ?
return false;
} else if (mOperationDone) {
return false;
}
}
return false;
}
mGetOperation为ClientOperation构造函数中的type。
public ClientOperation(int maxSize, ClientSession p, HeaderSet header, boolean type)
throws IOException {
mParent = p;
mEndOfBodySent = false;
mInputOpen = true;
mOperationDone = false;
mMaxPacketSize = maxSize;
mGetOperation = type;
...
}
前面有putOperation = (ClientOperation)mCs.put(request); type赋值为false。
ClientSession.java
public Operation put(HeaderSet header) throws IOException {
...
return new ClientOperation(mMaxTxPacketSize, this, head, false);
}
因此后面调用sendRequest(ObexHelper.OBEX_OPCODE_PUT);
后面和connect流程相同。