package jp.kshoji.blemidi.central;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.List;
import java.util.Set;
import jp.kshoji.blemidi.device.MidiInputDevice;
import jp.kshoji.blemidi.device.MidiOutputDevice;
import jp.kshoji.blemidi.listener.OnMidiDeviceAttachedListener;
import jp.kshoji.blemidi.listener.OnMidiDeviceDetachedListener;
import jp.kshoji.blemidi.listener.OnMidiScanStatusListener;
import jp.kshoji.blemidi.util.BleMidiDeviceUtils;
import jp.kshoji.blemidi.util.Constants;
/**
* Client for BLE MIDI Peripheral device service
*
* @author K.Shoji
*/
public final class BleMidiCentralProvider {
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final Handler handler;
private final BleMidiCallback midiCallback;
/**
* Callback for BLE device scanning
*/
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice bluetoothDevice, int rssi, byte[] scanRecord) {
if (bluetoothDevice.getType() != BluetoothDevice.DEVICE_TYPE_LE && bluetoothDevice.getType() != BluetoothDevice.DEVICE_TYPE_DUAL) {
return;
}
bluetoothDevice.connectGatt(context, true, midiCallback);
}
};
/**
* Callback for BLE device scanning (for Lollipop or later)
*/
private final ScanCallback scanCallback;
/**
* Constructor<br />
* Before constructing the instance, check the Bluetooth availability.
*
* @param context the context
*/
@SuppressLint("NewApi")
public BleMidiCentralProvider(@NonNull final Context context) throws UnsupportedOperationException {
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) == false) {
throw new UnsupportedOperationException("Bluetooth LE not supported on this device.");
}
bluetoothAdapter = ((BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
if (bluetoothAdapter == null) {
throw new UnsupportedOperationException("Bluetooth is not available.");
}
if (bluetoothAdapter.isEnabled() == false) {
throw new UnsupportedOperationException("Bluetooth is disabled.");
}
this.context = context;
this.midiCallback = new BleMidiCallback(context);
this.handler = new Handler(context.getMainLooper());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scanCallback = new ScanCallback() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
final BluetoothDevice bluetoothDevice = result.getDevice();
if (bluetoothDevice.getType() != BluetoothDevice.DEVICE_TYPE_LE && bluetoothDevice.getType() != BluetoothDevice.DEVICE_TYPE_DUAL) {
return;
}
if (!midiCallback.isConnected(bluetoothDevice)) {
bluetoothDevice.connectGatt(BleMidiCentralProvider.this.context, true, midiCallback);
}
}
}
};
} else {
scanCallback = null;
}
}
private volatile boolean isScanning = false;
/**
* Set if the Bluetooth LE device need `Pairing` <br />
* Pairing feature can be used on Android KitKat (API Level 19) or later.
*
* @param needsPairing if true, request paring with the connecting device
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public void setRequestPairing(boolean needsPairing) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Log.d(Constants.TAG, "Pairing feature is not supported on API Level " + Build.VERSION.SDK_INT);
return;
}
midiCallback.setNeedsBonding(needsPairing);
}
private Runnable stopScanRunnable = null;
/**
* Starts to scan devices
*
* @param timeoutInMilliSeconds 0 or negative value : no timeout
*/
@SuppressLint({ "Deprecation", "NewApi" })
public void startScanDevice(int timeoutInMilliSeconds) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
List<ScanFilter> scanFilters = BleMidiDeviceUtils.getBleMidiScanFilters(context);
ScanSettings scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback);
} else {
bluetoothAdapter.startLeScan(leScanCallback);
}
isScanning = true;
if (onMidiScanStatusListener != null) {
onMidiScanStatusListener.onMidiScanStatusChanged(isScanning);
}
if (stopScanRunnable != null) {
handler.removeCallbacks(stopScanRunnable);
}
if (timeoutInMilliSeconds > 0) {
stopScanRunnable = new Runnable() {
@Override
public void run() {
stopScanDevice();
isScanning = false;
if (onMidiScanStatusListener != null) {
onMidiScanStatusListener.onMidiScanStatusChanged(isScanning);
}
}
};
handler.postDelayed(stopScanRunnable, timeoutInMilliSeconds);
}
}
/**
* Stops to scan devices
*/
@SuppressLint({ "Deprecation", "NewApi" })
public void stopScanDevice() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
bluetoothAdapter.getBluetoothLeScanner().stopScan(scanCallback);
} else {
bluetoothAdapter.stopLeScan(leScanCallback);
}
} catch (Throwable ignored) {
// NullPointerException on Bluetooth is OFF
}
if (stopScanRunnable != null) {
handler.removeCallbacks(stopScanRunnable);
stopScanRunnable = null;
}
isScanning = false;
if (onMidiScanStatusListener != null) {
onMidiScanStatusListener.onMidiScanStatusChanged(isScanning);
}
}
/**
* Disconnects the specified device
*
* @param midiInputDevice the device
*/
public void disconnectDevice(@NonNull MidiInputDevice midiInputDevice) {
midiCallback.disconnectDevice(midiInputDevice);
}
/**
* Disconnects the specified device
*
* @param midiOutputDevice the device
*/
public void disconnectDevice(@NonNull MidiOutputDevice midiOutputDevice) {
midiCallback.disconnectDevice(midiOutputDevice);
}
/**
* Obtains the set of {@link jp.kshoji.blemidi.device.MidiInputDevice} that is currently connected
*
* @return unmodifiable set
*/
@NonNull
public Set<MidiInputDevice> getMidiInputDevices() {
return midiCallback.getMidiInputDevices();
}
/**
* Obtains the set of {@link jp.kshoji.blemidi.device.MidiOutputDevice} that is currently connected
*
* @return unmodifiable set
*/
@NonNull
public Set<MidiOutputDevice> getMidiOutputDevices() {
return midiCallback.getMidiOutputDevices();
}
private OnMidiScanStatusListener onMidiScanStatusListener;
/**
* Set the listener of device scanning status
*
* @param onMidiScanStatusListener the listener
*/
public void setOnMidiScanStatusListener(@Nullable OnMidiScanStatusListener onMidiScanStatusListener) {
this.onMidiScanStatusListener = onMidiScanStatusListener;
}
/**
* Set the listener for attaching devices
*
* @param midiDeviceAttachedListener the listener
*/
public void setOnMidiDeviceAttachedListener(@Nullable OnMidiDeviceAttachedListener midiDeviceAttachedListener) {
this.midiCallback.setOnMidiDeviceAttachedListener(midiDeviceAttachedListener);
}
/**
* Set the listener for detaching devices
*
* @param midiDeviceDetachedListener the listener
*/
public void setOnMidiDeviceDetachedListener(@Nullable OnMidiDeviceDetachedListener midiDeviceDetachedListener) {
this.midiCallback.setOnMidiDeviceDetachedListener(midiDeviceDetachedListener);
}
/**
* Terminates provider
*/
public void terminate() {
stopScanDevice();
midiCallback.terminate();
}
}