minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

Bluetooth とは、近距離の無線通信を行う規格の一つです。
これを使うことによって、他の Bluetooth 端末とデータの交換をワイヤレスで行うことができます。
point-to-point による1対1通信、および複数間通信に対応しています。

Bluetooth のAPIを使うことによって、以下のことが実現できます。

基本

まず、Bluetooth API を使うときに必要な4大作業を説明します。
Bluetooth のセットアップ、他デバイスの検知、デバイスへの接続、そしてデバイス間のデータ転送です。
全てのAPIは android.bluetooth パッケージにあります。

Bluetooth Permissions

アプリケーションで Bluetooth を使うためには、BLUETOOTH か BLUETOOTH_ADMIN のうち
少なくともどちらか一つの Permission を許可させなければなりません。

BLUETOOTH があれば、接続要求・接続の確保・データの転送といった作業は全て行うことが出来ます。
BLUETOOTH_ADMIN は、Bluetooth の設定を変更するような場合に必要な権限です。
ほとんどの場合、この権限を使う必要は無いはずです。
可能な限り、BLUETOOTH 権限を使って下さい。

AndroidManifest.xml

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest>

Bluetooth のセットアップ

Bluetooth を使った通信をする前に、その端末で Bluetooth 通信がサポートされているかを
確認しましょう。
サポートされていたら、それが有効になっていることも確認します。

もし Bluetooth がサポートされていない場合、そのアプリケーションで
Bluetooth 機能を使えないようにしましょう。

もしサポートされていても無効になっていたら、アプリケーションを継続させつつ
Bluetooth 機能を有効にするようアプリケーションが要求することができます。
これには二つの手順が必要です。

  1. BluetoothAdapter を得る

    まずは、BluetoothAdapter を取得します。

    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    

    もし NULL が取得できた場合、Bluetooth がサポートされていません。残念でした。

  2. Bluetooth を有効にする

    次に、Bluetooth が有効になっているかを確認します。
    もし有効になっていなければ、有効にするよう要求を出します。

    if (!mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    このように、Activity に対して Intent を送るだけです。
    こうすると、Android はユーザに対して Bluetooth を有効にしていいか確認するダイアログを出します。

    もしユーザが OK すれば、Activity に RESULT_OK が返ります。
    NG だったら、RESULT_CANCELED が返ります。
    これらは onActivityResult() メソッドを実装することでハンドリングできます。

またこれ以外にも、アプリケーションの実行中に Bluetooth の状況が変化することがあります。
これをハンドリングするには ACTION_STATE_CHANGED のブロードキャストIntentを受信するようにします。

AndroidManifest.xml

<receiver android:name="com.example.project.ReportReceiver">
    <intent-filter>
        <action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
    </intent-filter>
</receiver>

ReportReceiver.java

public class ReportReceiver extends BroadcastReceiver {
    public void onReceive (Context context, Intent intent) {
        // 受信時の処理
        Bundle extras = intent.getExtras();
        int state = extras.getInt(BluetoothAdapter.EXTRA_STATE);
        int prevState = extras.getInt(BluetoothAdapter.EXTRA_PREVIOUS_STATE);
      }
}

このとき、Intent の EXTRA_STATE および EXTRA_PREVIOUS_STATE に Bluetooth の状況が格納されています。

"discoverability" を有効にすることによって、Bluetooth を自動的に有効にすることができます。
この場合、上で紹介した二つの手順は必要ありません。
この機能については後述します。

デバイスの検出

BluetoothAdapter を使うことによって、他の Bluetooth デバイスを探すことができます。

Paired デバイスの問い合わせ

Bluetooth には、デバイス同士を組み合わせる(Paired)という概念があります。
これによって組み合ったデバイス同士でのみ、両者の通信が可能になります。

他のデバイスの検出をする前に、他から自分に対して接続要求しているデバイスが無いか
確認するのは良い方法です。

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
    // ListView にデバイスの名称とアドレスを追加
    mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}

このように、getBondedDevices() を使います。
これで、接続要求している他デバイスをリストアップすることができます。

この段階ではまだこれらのデバイスに接続はしていません。
後に接続を初期化することになりますが、そのときに必要なものはMACアドレスだけです。

近くのデバイスを発見する

startDiscovery() の呼び出しによって、デバイスを発見するためのプロセスが始まります。
このプロセスは非同期で動きます。
上記メソッドの呼び出しはプロセスを開始する初期処理が完了した時点ですぐさま処理が返り、
初期処理が正常に完了したかどうかが戻り値として渡されます。

このプロセスは通常、約12秒間に渡って調査スキャンを遂行します。
それが終わると、見つかったそれぞれのデバイスに対して Bluetooth 名を割り当てるページスキャンをします。

アプリケーションには必ず ACTION_FOUND の Intent を受信する BroadcastReceiver を登録しておく必要があります。

AndroidManifest.xml

<receiver android:name="com.example.project.FoundReceiver">
    <intent-filter>
        <action android:name="android.bluetooth.device.action.FOUND" />
    </intent-filter>
</receiver>

FoundReceiver.java

public class FoundReceiver extends 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);
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
}

これで、近くのデバイスが発見されるとそれを検知することができます。
前述したように、接続を初期化するために必要なものは、MACアドレスだけです。
ですから、ここで保持しておく必要があるものは device.getAddress() だけです。
※ device を保持しておく必要は無い

注意点

デバイスの発見プロセスは、非常に重い処理になります。
これには多くのリソース(バッテリー)を消費するということを覚えておいて下さい。

発見プロセス中にデバイスを発見して、もう他のデバイスを発見する必要が無い場合は
すぐさま BluetoothAdapter.cancelDiscovery() を呼び出して発見プロセスを停止して下さい。
これによって、放っておくと12秒間続くこのプロセスを強制的に停止させることができます。
これはリソースの大きな節約になります。

また、既にデバイスに接続をしている状態で発見プロセスを走らせることも避けて下さい。
この行為は、その接続が利用できる帯域幅を激減させることになります。

discoverability を有効にする

もし自分のデバイスを他のデバイスに発見してほしい場合、
ACTION_REQUEST_DISCOVERABLE アクションを持った Intent で
startActivityForResult(Intent, int) を呼び出して下さい。

これによって、アプリケーションの動きを止めることなく
自身を発見可能なモードを有効にすることができます。

デフォルトでは、このモードは120秒間続きます。
Intent に EXTRA_DISCOVERABLE_DURATION の属性を追加することで、最大300秒までこの時間を変化させることができます。

discoverability を有効にするとき、ユーザに確認のダイアログが表示されます。
ここには discoverability を有効にすることが、モードの継続時間と共に表示されています。
ユーザが承認/拒否すると、Activity はその結果を受け取ります。

これを受け取るには、Activity.onActivityResult() メソッドを実装します。
拒否された場合、resultCode には RESULT_CANCELLED が入っています。
承認された場合、resultCode にはモードの継続時間と同じ値(秒数)が入っています。

もしそのデバイスがまだ Bluetooth を有効にしていなかった場合
discoverability を有効にすると自動的に Bluetooth も有効になります。

デバイスは、このモードを120秒間そっと有効にします。
ACTION_SCAN_MODE_CHANGED の BroadcastReceiver を登録しておくことで
モード状態の変化をキャッチすることができます。

このとき、渡されてくる Intent には EXTRA_SCAN_MODE と EXTRA_PREVIOUS_SCAN_MODE の追加属性が入っています。

リモートデバイスの接続を初期化する段階では、discoverability を有効にする必要はありません。
discoverability の有効化は、あなたのアプリケーションを
自身に向かって発信された接続要求を許可するようなサーバアプリケーションにしたい場合に限って必要になります。
なぜなら、接続を初期化する段階では既にデバイスを発見しているはずだからです。
ここで(この期に及んで)自身を発見してもらう必要性は、無いはずです。

デバイスへの接続

二つのデバイス上にあるアプリケーション間で接続を確立させるためには、
サーバ側とクライアント側の両方を実装しなければなりません。

なぜなら、一方のデバイスはサーバソケットを開く必要があり、
もう片方は(サーバ側デバイスのMACアドレスを使って)接続を初期化する必要があるからです。

サーバとクライアントはお互いを接続し合うことを考慮します。
そのとき、それぞれが同じ RFCOMM チャンネルの中で接続された BluetoothSocket を持ちます。

ここでのポイントは、両者のデバイスは入力と出力のストリームを得ることができて初めて、
データの転送を開始することができるということです。
詳しくは、接続の管理 の項で説明します。
この項では、どのように両者のデバイスの接続を初期化するかについて説明します。

それぞれの接続方法

サーバデバイスとクライアントデバイスはそれぞれ異なる方法で、
必要とされる BluetoothSocket を取得します。

サーバは、他から発信された接続が許可されると、このソケットを受信します。
クライアントは、RFCOMM チャンネルをサーバに開放すると、このソケットを受信します。

一つの実装テクニックは、自動的に両者のデバイスをサーバとして準備することです。
そして、それぞれがサーバソケットを開き、そこへの接続を待ちます。
このとき、どちらのデバイスとも他者との接続を初期化することができるし、クライアントになることもできます。

一つのデバイスは接続を提供し、要求に応じてサーバソケットを開きます。
他のデバイスは単純にその接続を初期化します。

ワンポイント

もし二つのデバイスが以前に一度もペアになったことが無い場合、
Android フレームワークは自動的にペア要求通知ダイアログを表示します。
デバイスへの接続を試みるとき、アプリケーションはデバイス同士がペアかどうかを考慮する必要はありません。
あなたの RFCOMM 接続試行は、ユーザがペア接続に成功するまでブロックされます。
もしユーザがペアを拒否すれば、試行は失敗します。
ペア接続に失敗したり、タイムアウトになることもあります。

サーバ側の接続処理

二つのデバイスを接続したいとき、一つは
BluetoothServerSocket を開放し続けることでサーバとして振舞う必要があります。
このサーバソケットは、外から発信された接続要求を聞き、
要求を許可するときに接続用の BluetoothSocket を提供します。

BluetoothServerSocket を用いて BluetoothSocket を提供したら
(さらに多くの接続を許可したい場合を除いて)BluetoothServerSocket は無効にするべきです。

サーバソケットのセットアップと接続許可について、基本的な手順を紹介します。

  1. サーバソケットの取得

    listenUsingRfcommWithServiceRecord(String, UUID) を呼び出して、BluetoothServerSocket を取得します。

    String には、このサービス名を識別する文字列を渡します。
    システムは自動的にこの文字列をデバイス上の
    SDP(Service Discovery Protocol) データベースエントリに書き込みます。
    この文字列は任意の値です。単純にアプリケーション名と同じで構いません。

    UUID もまたSDPのエントリに格納されます。
    これは主にクライアントデバイスとの接続合意ロジックで使われます。
    クライアントがこのデバイスに接続を試みるとき、
    接続要求をするサービスを一意に決めるために UUID が使われます。

    例えば一つのアプリケーションで複数のサービスを提供するような場合
    それぞれは異なる UUID を持っていなければなりません。

  2. 接続要求の受信開始

    accept() を呼び出して、接続要求の受信を開始します。
    この呼び出しは、いずれかの接続が許可されたり例外が発生したときに処理が返ります。
    ※ それまでは、処理は戻ってきません

    クライアント側のリモートデバイスが接続要求を送信するとき、UUID も一緒に送信します。
    この UUID は、上で説明したサーバソケットに登録した値と一致する必要があります。
    許可に成功すると、accept() 呼び出しから処理が返り、接続した BluetoothSocket が返されます。

  3. サーバソケットの開放

    もしそれ以上接続を許可しない場合は、close() を呼び出してサーバソケットを開放します。
    これによって、サーバソケットとそれに関連するリソースを全て開放します。
    ただし、上で接続した BluetoothSocket は閉じません。

    RFCOMM は TCP/IP と異なり、同時に一つのチャンネルで一つのクライアント接続しか許可しません。
    ですから、ほとんどの場合はここでサーバソケットを開放することになります。
    つまり、接続に成功したらすぐにサーバソケットを閉じるのです。

accept() の呼び出しは、メイン Activity のUIスレッド内で実行すべきではありません
なぜなら、この呼び出しはブロッキングコールなので、呼び出しから戻ってくるまでの間
アプリケーションの全ての描画処理、キー入力処理などができなくなってしまうからです。
普通は、新しくスレッドを作ってそこで BluetoothServerSocket や BluetoothSocket に関する
全ての処理を行うようにします。

accept() のようなブロック呼び出しを中断するためには、
他のスレッドから close() を呼び出します。すると、このブロック呼び出しから即座に処理が返ってきます。
BluetoothServerSocket や BluetoothSocket の全メソッドは、スレッドセーフで実装されています。

サンプル

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
    
    public AcceptThread() {
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID は、アプリケーションの UUID 文字列です。この値は、クライアント側でも使われます
            tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
    
    public void run() {
        BluetoothSocket socket = null;
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            if (socket != null) {
                manageConnectedSocket(socket); // 接続を管理する処理
                mmServerSocket.close();
                break;
            }
        }
    }
    
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

まずコンストラクタ内で、サーバソケットを作成します。
run() の中で、クライアントからの接続を待ちます。
接続が成功したら、(必要に応じてその接続を管理する処理をした後で)サーバソケットを閉じます。
例外やタイムアウトが発生したら、何もせずに終了します。

この例では、一つのクライアント接続のみが想定されています。
accept() が返した接続は既に開いているので、それに対して connect() を呼び出さないで下さい。

途中でサーバソケットを閉じたくなったら、外から明示的に cancel() を呼び出します。
もし accept() 中にこれが呼び出されると、おそらく IOException が発生するでしょう。

クライアント側の接続処理

リモートの(サーバソケットを開いている)デバイスとの接続を初期化するために、
まず最初にそのリモートデバイスを表現する BluetoothDevice を取得する必要があります。
この方法について詳しくは「デバイスの検出」を読んで下さい。
そしたら BluetoothDevice を使って BluetoothSocket を得て接続を初期化します。

基本的な流れは以下になります。

  1. BluetoothSocket の取得

    BluetoothDevice.createRfcommSocketToServiceRecord(UUID) を使って、BluetoothSocket を取得します。
    UUID には、サーバ側でサーバソケットを作成するときに使った値と同じものを使います。
    アプリケーションに定数として UUID を定義しておき、サーバとクライアントでこの定数を使うのが
    単純な方法でしょう。

  2. 接続の初期化

    BluetoothSocket.connect() を呼び出して、接続を初期化します。
    これによって、システムはリモートデバイス上の SDP をルックアップして UUID のマッチングを行います。
    ルックアップが成功しリモートデバイスが接続を許可すれば、
    その接続間で使用する RFCOMM チャンネルが共有され、connect() から処理が返ります。

    この呼び出しはブロッキングコールです。
    もしあらゆる理由において接続が失敗したり、12秒経ってメソッド呼び出しがタイムアウトすると
    このメソッド呼び出しは例外を発生します。
    ブロッキングコールなので、この処理はメインスレッドとは別のスレッド上で実行する必要があります。

ワンポイント

connect() を呼び出すときに必ず、デバイス発見処理をしていないことを確認して下さい。
もし発見処理が走っていると、接続の試行処理は極端に遅くなり、失敗しやすくなります。

サンプル

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
    
    public ConnectThread(BluetoothDevice device) {
        BluetoothSocket tmp = null;
        mmDevice = device;
        try {
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
    
    public void run() {
        mAdapter.cancelDiscovery();
        try {
            mmSocket.connect();
        } catch (IOException connectException) {
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
        manageConnectedSocket(mmSocket);
    }
    
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

サーバ側と大体同じです。
コンストラクタ内でクライアント用のソケットを作成し、run() で接続します。
クライアント特有なのは、最初に cancelDiscovery() を呼んで発見処理をキャンセルしていることです。
あと、接続に成功した後もソケットは閉じてはいけません。

接続する必要が無くなったら必ず、外から明示的に cancel() を呼び出しましょう。
これによって、即座にソケットが閉じられ、関連する全てのリソースが開放されます。

接続の管理

二つ(またはそれ以上)のデバイスの接続が成功したら、そのそれぞれが接続された BluetoothSocket を持ちます。
ここからが楽しいところです。デバイス間でデータを共有できるのです。
BluetoothSocket を使って任意のデータを転送する一般的な方法は以下になります。

  1. ストリームの取得

    データ転送用の InputStream と OutputStream を取得します。
    これには getInputStream() と getOutputStream() を呼び出します。

  2. データの入出力

    InputStream.read(byte[]) でデータを読み込み、OutputStream.write(byte[]) でデータを書き込みます。

これで全てです。

もちろん、実装の詳細は充分に考慮する必要があるでしょう。
まず第一に、ストリームに対する入出力処理は全て専用のスレッドを使うべきです。

これは重要なことです。read も write もブロッキングメソッドだからです。
read はストリームから何かデータが取得できるまでブロックされます。
write は通常はブロックされませんが、もしリモートデバイスが充分に早くread を呼び出さなかったり
中間バッファが一杯になった場合などにブロックされることがあります。

ですから、読み込みを行うスレッドのメインループは専用であるべきです。
そのスレッド内の別メソッドで、書き込みを行うことになるでしょう。

サンプル

リモートデバイスからデータを受信する例を紹介します。

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
    
    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
    
    public void run() {
        byte[] buffer = new byte[1024];
        int bytes;
        while (true) {
            try {
                bytes = mmInStream.read(buffer);
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
    
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
    
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

コンストラクタで、ソケットの InputStream, OutputStream を取得しています。
run() で、入力ストリームからデータを読み込み、それを mHandler 経由で Activity に送信しています。
mHandler は Activity のメインスレッドで作成されたものになるでしょう。
※ Handler は、異なるスレッドで動くインスタンスにデータを送るための良い手段です

相手からデータが送信されるまで、read() メソッドはブロックされます。
データが(1byte以上)送信されたら、read() から処理が返ってきます。
1024バイトを超えるデータが送信された場合は、1024バイトずつ read() メソッドは処理を返すでしょう。

ただしこの動作は保障されているわけではなく、実際には512バイトずつかもしれませんし
もしかしたら1バイトずつかもしれません。

リモートデバイスへのデータ送信は、受信よりずっと簡単で write() を呼び出すだけです。

cancel() メソッドも重要です。
先ほども説明したように、データの送受信が完了したら
必ず cancel() を呼び出してソケットを閉じることを忘れないで下さい。