UnityでAndroidのBLEを使うネイティブプラグインを作る

(2016-10-23)

UnityからBLEを使うためのネイティブプラグインを作る。

Android側

まず、Activityなしのプロジェクトを作って、New ModuleからAndroid Libraryを選択。 これらのパッケージ名がUnityで使うものと被らないようにする。

/Applications/Unity/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar をModuleの方のlibsに置く。

import com.unity3d.player.UnityPlayer;

このjarは元々のやつとかぶってしまうので除外(build.gradleに追加)

android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

ActiviyはUnityPlayer.currentActivityで取得でき、 Unity側のメソッドを呼ぶのも UnityPlayer.UnitySendMessage(mGameObjName, mCallBackName, new String(characteristic.getValue())); のようにできる。

public class BLE {

    private final static String TAG = BLE.class.getSimpleName();
    private static final int REQUEST_ENABLE_BT = 1;
    private static final int MY_PERMISSION_RESPONSE = 2;

    private static final String PERIPHERAL_LOCAL_NAME = "my-ble";
    private static final UUID PERIPHERAL_SERIVCE_UUID = UUID.fromString("BF9CB85F-620C-4A67-BDD2-1A64213F74CA");
    private static final UUID PERIPHERAL_CHARACTERISTIC_UUID = UUID.fromString("5F83E23F-BCA1-42B3-B6F2-EA82BE46A93D");
    private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private String mGameObjName;
    private String mCallBackName;

    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothGatt mBluetoothGatt;
    private BluetoothGattCharacteristic mCharacteristic;
    private Handler mHandler;

    // Stops scanning after 30 seconds.
    private static final long SCAN_PERIOD = 30000;

    public BLE(String gameObjName, String callBackName) {

        this.mGameObjName = gameObjName;
        this.mCallBackName = callBackName;

        mHandler = new Handler();

        if (!UnityPlayer.currentActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Toast.makeText(UnityPlayer.currentActivity, "BLEをサポートしていません", Toast.LENGTH_SHORT).show();
            UnityPlayer.currentActivity.finish();
            return;
        }

        final BluetoothManager bluetoothManager =
                (BluetoothManager) UnityPlayer.currentActivity.getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter();

        if (mBluetoothAdapter == null) {
            Toast.makeText(UnityPlayer.currentActivity, "Bluetoothをサポートしていません", Toast.LENGTH_SHORT).show();
            UnityPlayer.currentActivity.finish();
            return;
        }

        onActive();
    }

    public void onActive() {
        Log.d(TAG, "onActive");
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            UnityPlayer.currentActivity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }

        scanLeDevice(true);
    }

    public void onPause() {
        Log.d(TAG, "onPause");
        scanLeDevice(false);

        if(mCharacteristic != null){
            mBluetoothGatt.setCharacteristicNotification(
                    mCharacteristic,
                    false
            );
        }

        if(mBluetoothGatt != null) {
            mBluetoothGatt.close();
            mBluetoothGatt = null;
        }
    }

    private void scanLeDevice(final boolean enable) {
        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    }

    private BluetoothAdapter.LeScanCallback mLeScanCallback =
            new BluetoothAdapter.LeScanCallback() {

                @Override
                public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
                    if(PERIPHERAL_LOCAL_NAME.equals(device.getName())){
                        scanLeDevice(false);
                        connect(device);
                    }
                }
            };

    private boolean connect(BluetoothDevice device) {
        if (mBluetoothAdapter == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            return false;
        }

        // Previously connected device.  Try to reconnect.
        if (mBluetoothGatt != null) {
            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
            if (mBluetoothGatt.connect()) {
                return true;
            } else {
                return false;
            }
        }

        mBluetoothGatt = device.connectGatt(UnityPlayer.currentActivity, false, mGattCallback);
        Log.d(TAG, "Trying to create a new connection.");
        return true;
    }


    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {

                scanLeDevice(false);
                Log.i(TAG, "Connected to GATT server.");
                gatt.discoverServices();

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(TAG, "Disconnected from GATT server.");
            } else{
                Log.i(TAG, "onConnectionStateChange:" + newState);
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                mCharacteristic = gatt.getService(PERIPHERAL_SERIVCE_UUID).
                        getCharacteristic(PERIPHERAL_CHARACTERISTIC_UUID);

                gatt.setCharacteristicNotification(
                        mCharacteristic,
                        true
                );
                BluetoothGattDescriptor descriptor = mCharacteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG);
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                mBluetoothGatt.writeDescriptor(descriptor);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            UnityPlayer.UnitySendMessage(mGameObjName, mCallBackName, new String(characteristic.getValue()));
        }
    };

}

Manifestに追加した分。

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

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

できたらaarを生成する。

$ ./gradlew assembleRelease

build/outputs/aar/*-release.aarをUnityのAssets/Plugins/Android/libsに置く。

あと、依存aarはこの中に含まれないようなのでそれもまとめてコピーする必要がある。

task copyLibs(type: Copy) {
    from configurations.compile
    into 'build/outputs/aar'
    exclude { details -> details.file.name.endsWith(".jar") }
}

Manifetstのmergeに失敗したのでSDKVersionを合わせる。

compileSdkVersion 22

defaultConfig {
    minSdkVersion 19
    targetSdkVersion 22
...

Unity側

こんな感じでインスタンスを作り、 メソッドを呼べる。 ただし、unity editor上ではInit'd AndroidJavaClass with null ptr!のエラーが出る。

plugin = new AndroidJavaObject("net.sambaiz.unity_ble.BLE", this.gameObject.name, "received");

void received(string message){
	Debug.Log ("BLE:" + message);
}

void OnApplicationPause (bool pauseStatus)
{
	if (pauseStatus) {
		plugin.Call ("onPause");
	} else {
		plugin.Call ("onActive");
	}
}