/**
* Copyright (C) 2013 - 2015 the enviroCar community
* <p>
* This file is part of the enviroCar app.
* <p>
* The enviroCar app is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* The enviroCar app is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License along
* with the enviroCar app. If not, see http://www.gnu.org/licenses/.
*/
package org.envirocar.app.handler;
import android.app.Activity;
import android.app.ActivityManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.google.common.base.Preconditions;
import com.squareup.otto.Bus;
import org.envirocar.app.services.OBDConnectionService;
import org.envirocar.core.events.bluetooth.BluetoothDeviceDiscoveredEvent;
import org.envirocar.core.events.bluetooth.BluetoothDeviceSelectedEvent;
import org.envirocar.core.events.bluetooth.BluetoothStateChangedEvent;
import org.envirocar.core.injection.InjectApplicationScope;
import org.envirocar.core.logging.Logger;
import org.envirocar.core.utils.BroadcastUtils;
import org.envirocar.core.utils.ServiceUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.schedulers.Schedulers;
/**
* @author dewall
*/
@Singleton
public class BluetoothHandler {
private static final Logger LOGGER = Logger.getLogger(BluetoothHandler.class);
private final Context context;
private final Bus bus;
private final Scheduler.Worker mWorker = Schedulers.io().createWorker();
private Subscription mDiscoverySubscription;
private boolean mIsAutoconnecting;
// The bluetooth adapter
private final BluetoothAdapter mBluetoothAdapter;
protected final BroadcastReceiver mBluetoothStateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.STATE_TURNING_OFF:
LOGGER.debug("Bluetooth State Changed: STATE_TURNING_OFF");
stopBluetoothDeviceDiscovery();
if (mDiscoverySubscription != null) {
mDiscoverySubscription.unsubscribe();
mDiscoverySubscription = null;
}
break;
case BluetoothAdapter.STATE_OFF:
LOGGER.debug("Bluetooth State Changed: STATE_OFF");
// Post a new event for the changed bluetooth state on the eventbus.
BluetoothStateChangedEvent turnedOffEvent =
new BluetoothStateChangedEvent(false);
bus.post(turnedOffEvent);
break;
case BluetoothAdapter.STATE_TURNING_ON:
LOGGER.debug("Bluetooth State Changed: STATE_TURNING_ON");
break;
case BluetoothAdapter.STATE_ON:
LOGGER.debug("Bluetooth State Changed: STATE_ON");
// Post a new event for the changed bluetooth state on the eventbus.
BluetoothStateChangedEvent turnedOnEvent
= new BluetoothStateChangedEvent(true);
bus.post(turnedOnEvent);
break;
default:
LOGGER.debug("Bluetooth State Changed: unknown state");
break;
}
}
}
};
/**
* Constructor
*
* @param context the context of the current scope.
*/
@Inject
public BluetoothHandler(@InjectApplicationScope Context context, Bus bus) {
this.context = context;
this.bus = bus;
// Get the default bluetooth adapter.
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// Register ourselves on the eventbus.
this.bus.register(this);
// Register this handler class for Bluetooth State Changed broadcasts.
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
this.context.registerReceiver(mBluetoothStateChangedReceiver, filter);
}
/**
* Starts the connection to the bluetooth device if not already active.
*/
public void startOBDConnectionService() {
if (!ServiceUtils.isServiceRunning(context, OBDConnectionService.class))
context.getApplicationContext()
.startService(new Intent(context, OBDConnectionService.class));
}
public void stopOBDConnectionService() {
if (ServiceUtils.isServiceRunning(context, OBDConnectionService.class)) {
context.getApplicationContext()
.stopService(new Intent(context, OBDConnectionService.class));
}
ActivityManager amgr = (ActivityManager) context.getSystemService(Context
.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> list = amgr.getRunningAppProcesses();
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ActivityManager.RunningAppProcessInfo apinfo = list.get(i);
String[] pkgList = apinfo.pkgList;
if (apinfo.processName.startsWith("org.envirocar.app.services.OBD")) {
for (int j = 0; j < pkgList.length; j++) {
amgr.killBackgroundProcesses(pkgList[j]);
}
}
}
}
}
/**
* Returns the corresponding BluetoothDevice for the attributes stored in the shared
* preferences.
*
* @return The BluetoothDevice for the selected OBDII adapter in the shared preferences.
*/
public BluetoothDevice getSelectedBluetoothDevice() {
// No Bluetooth is available. Therefore, return null.
if (!isBluetoothEnabled())
return null;
// Get the preferences of the device.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String deviceName = preferences.getString(
PreferenceConstants.PREF_BLUETOOTH_NAME,
PreferenceConstants.PREF_EMPTY);
String deviceAddress = preferences.getString(
PreferenceConstants.PREF_BLUETOOTH_ADDRESS,
PreferenceConstants.PREF_EMPTY);
// If the device address is not empty and the device is still a paired device, get the
// corresponding BluetoothDevice and return it.
if (!deviceAddress.equals(PreferenceConstants.PREF_EMPTY)) {
Set<BluetoothDevice> devices = getPairedBluetoothDevices();
for (BluetoothDevice device : devices) {
if (device.getAddress().equals(deviceAddress))
return device;
}
// The device is not paired anymore. Therefore, delete everything in the shared
// preferences related to the preference.
setSelectedBluetoothDevice(null);
}
return null;
}
public void setSelectedBluetoothDevice(BluetoothDevice selectedDevice) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean success = preferences.edit()
.remove(PreferenceConstants.PREF_BLUETOOTH_NAME)
.remove(PreferenceConstants.PREF_BLUETOOTH_ADDRESS)
.commit();
if (selectedDevice != null) {
// Update the shared preference entry for the bluetooth selection tag.
success &= preferences.edit()
.putString(PreferenceConstants.PREF_BLUETOOTH_NAME,
selectedDevice.getName())
.putString(PreferenceConstants.PREF_BLUETOOTH_ADDRESS,
selectedDevice.getAddress())
.commit();
}
if (success) {
LOGGER.info("Successfully updated shared preferences");
bus.post(new BluetoothDeviceSelectedEvent(selectedDevice));
}
}
/**
* Returns the BluetoothDevice of a given address.
*
* @param address the address of the required BluetoothDevice.
* @return the BluetoothDevice of the given address.
*/
public BluetoothDevice getBluetoothDeviceByAddress(String address) {
// No Bluetooth is available. Therefore, return null.
if (!isBluetoothEnabled())
return null;
// If the device is still a paired device, get the corresponding BluetoothDevice
// and return it.
Set<BluetoothDevice> devices = getPairedBluetoothDevices();
for (BluetoothDevice device : devices) {
if (device.getAddress().equals(address))
return device;
}
return null;
}
/**
* Starts the Bluetooth discovery for a specific input device. If the device has been
* successfull discovered, the callback's onActionDeviceDiscovered is called.
*
* @param inputDevice The input device to start a discovery for.
* @param callback The callback used to call back information at some convenient time.
*/
public void startDiscoveryForSingleDevice(final BluetoothDevice inputDevice,
final BluetoothDeviceDiscoveryCallback callback) {
// First check the input paramters to be not null.
Preconditions.checkNotNull(inputDevice, "Input device cannot be null");
Preconditions.checkNotNull(callback, "Input callback cannot be null");
// Uses the normal discovery routing with a fresh callback that filters the discovered
// devices based on their address.
startBluetoothDeviceDiscovery(false, new BluetoothDeviceDiscoveryCallback() {
@Override
public void onActionDeviceDiscoveryStarted() {
// forward callback call
callback.onActionDeviceDiscoveryStarted();
}
@Override
public void onActionDeviceDiscoveryFinished() {
// forward callback call
callback.onActionDeviceDiscoveryFinished();
}
@Override
public void onActionDeviceDiscovered(BluetoothDevice device) {
// If the address of the input device matches the discovered device, then return
// the device over the callback.
if (device.getAddress().equals(inputDevice.getAddress())) {
callback.onActionDeviceDiscovered(device);
}
}
});
}
/**
* Returns an Observable that will execute the bluetooth discovery for a specific input
* device.
*
* @param inputDevice the Bluetooth device to search for.
* @return an observable that searches for a specific device.
*/
public Observable<BluetoothDevice> startBluetoothDiscoveryForSingleDevice(
BluetoothDevice inputDevice) {
return startBluetoothDiscovery()
.filter(device1 -> inputDevice.getAddress().equals(device1.getAddress()));
}
/**
* Returns an Observable that will execute the search for unpaired devices.
*
* @return an observable that executes the search for unpaired devices.
*/
public Observable<BluetoothDevice> startBluetoothDiscoveryOnlyUnpaired() {
return startBluetoothDiscovery()
.filter(device -> device.getBondState() != BluetoothDevice.BOND_BONDED);
}
/**
* @return
*/
public Observable<BluetoothDevice> startBluetoothDiscovery() {
return Observable.create(subscriber -> {
LOGGER.info("startBluetoothDiscovery(): subscriber call");
// If the device is already discovering, cancel the discovery before starting.
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
// Small timeout such that the broadcast receiver does not receive the first
// ACTION_DISCOVERY_FINISHED
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (mDiscoverySubscription != null) {
// Cancel the pending subscription.
mDiscoverySubscription.unsubscribe();
mDiscoverySubscription = null;
}
// Register for broadcasts when a device is discovered or the discovery has finished.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
mDiscoverySubscription = BroadcastUtils
.createBroadcastObservable(context, filter)
.subscribe(new Subscriber<Intent>() {
@Override
public void onCompleted() {
LOGGER.info("onCompleted()");
subscriber.onCompleted();
}
@Override
public void onError(Throwable e) {
LOGGER.info("onError()");
subscriber.onError(e);
}
@Override
public void onNext(Intent intent) {
String action = intent.getAction();
LOGGER.info("Discovery: received action = " + action);
// If the discovery process has been started.
if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
subscriber.onStart();
}
// If the discovery process finds a device
else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice from the intent.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice
.EXTRA_DEVICE);
// and inform the subscriber.
subscriber.onNext(device);
}
// If the discovery process has been finished.
else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
subscriber.onCompleted();
mWorker.schedule(() -> {
if (!isUnsubscribed()) {
unsubscribe();
}
}, 100, TimeUnit.MILLISECONDS);
}
}
});
subscriber.add(mDiscoverySubscription);
mBluetoothAdapter.startDiscovery();
});
}
/**
* Registers the broadcast receiver for discovery related actions and starts
* the discovery of other devices. This method filters the paired devices returned over the
* callback so that no paired device is gonna returned.
*
* @param callback the callback instance to get responses over.
*/
public void startBluetoothDeviceDiscovery(final BluetoothDeviceDiscoveryCallback callback) {
// check required parameters to be not null
Preconditions.checkNotNull(callback, "Error: Input callback cannot be null");
// Call the overloaded method
startBluetoothDeviceDiscovery(true, callback);
}
/**
* Registers the broadcast receiver for discovery related actions and starts
* the discovery of other devices.
*
* @param filterPairedDevices true, if the callback only returns discovered devices that are
* not paired.
* @param callback the callback instance to get responses over.
*/
public void startBluetoothDeviceDiscovery(final boolean filterPairedDevices,
final BluetoothDeviceDiscoveryCallback callback) {
Preconditions.checkNotNull(mBluetoothAdapter, "Error BluetoothAdapter has to be " +
"initialized before");
Preconditions.checkNotNull(callback, "Error: Input callback cannot be null");
// If the device is already discovering, cancel the discovery before starting.
stopBluetoothDeviceDiscovery();
// Register for broadcasts when a device is discovered or the discovery has finished.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
// Register a receiver.
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// If the discovery process finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice from the intent.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice
.EXTRA_DEVICE);
// If the device
boolean newDevice = (!filterPairedDevices) || (filterPairedDevices && device
.getBondState() != BluetoothDevice.BOND_BONDED);
if (newDevice) {
BluetoothDeviceDiscoveredEvent event =
new BluetoothDeviceDiscoveredEvent(device);
callback.onActionDeviceDiscovered(device);
}
}
// If the discovery process has been started.
else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
callback.onActionDeviceDiscoveryStarted();
}
// If the discovery process has been finished.
else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
callback.onActionDeviceDiscoveryFinished();
BluetoothHandler.this.context.unregisterReceiver(this);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
// Nothing to do yet
}
}
}, filter);
mBluetoothAdapter.startDiscovery();
}
/**
* Cancels the disovery of other Bluetooth devices if the bluetooth device is currently in
* the device discovery process.
*/
public void stopBluetoothDeviceDiscovery() {
// Cancel discovery if it is discovering.
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
LOGGER.info("Bluetooth discovery cancled");
}
}
/**
* Return the set of {@link BluetoothDevice} objects that are bonded
* (paired) to the local adapter.
*
* @return the set of already paired Bluetooth devices.
*/
public Set<BluetoothDevice> getPairedBluetoothDevices() {
return mBluetoothAdapter.getBondedDevices();
}
public boolean isAutoconnecting() {
return mIsAutoconnecting;
}
public boolean isBluetoothEnabled() {
if (mBluetoothAdapter != null)
return mBluetoothAdapter.isEnabled();
return false;
}
public boolean isBluetoothActive() {
if (mBluetoothAdapter == null || mBluetoothAdapter.getAddress() == null) {
return false;
}
return true;
}
public void enableBluetooth(Activity activity) {
// If Bluetooth is not enabled, request that it will be enabled.
if (mBluetoothAdapter != null && !mBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
activity.startActivityForResult(enableIntent, -1);
}
}
/**
* Return true if the local Bluetooth adapter is currently in the device
* discovery process.
*
* @return true if discovering.
*/
public boolean isDiscovering() {
return mBluetoothAdapter.isDiscovering();
}
public void disableBluetooth(Activity activity) {
// If Bluetooth is enabled, request that it will be enabled.
if (isBluetoothEnabled()) {
mBluetoothAdapter.disable();
}
}
/**
* Initiates the pairing process to a given {@link BluetoothDevice}.
*
* @param device the device to pair to.
* @param callback the callback listener.
*/
public void pairDevice(final BluetoothDevice device,
final BluetoothDevicePairingCallback callback) {
// Register a new BroadcastReceiver for BOND_STATE_CHANGED actions.
IntentFilter intent = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// if the action is a change of the pairing state
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
// Get state and previous state.
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
final int prevState = intent.getIntExtra(
BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR);
if (state == BluetoothDevice.BOND_BONDED &&
prevState == BluetoothDevice.BOND_BONDING) {
// The device has been successfully paired, inform the callback about
// the successful pairing.
callback.onDevicePaired(device);
} else if (state == BluetoothDevice.BOND_NONE && prevState == BluetoothDevice
.BOND_BONDING) {
// It was not able to successfully establishing a pairing to the given
// device. Inform the callback
callback.onPairingError(device);
}
}
}
}, intent);
// Using reflection to invoke "createBond" method in order to pair with a given device.
// This method is public in API lvl 18.
try {
// Invoke method and get return value
Method method = device.getClass().getMethod("createBond", (Class[]) null);
boolean value = (boolean) method.invoke(device, (Object[]) null);
// Check error.
if (value)
callback.onPairingStarted(device);
else
callback.onPairingError(device);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* Removes the pairing to a given {@link BluetoothDevice}.
*
* @param device the device to which the pairing should be removed.
* @param callback the callback listener to inform about successes or errors.
*/
public void unpairDevice(final BluetoothDevice device, final BluetoothDeviceUnpairingCallback
callback) {
// Register a new BroadcastReceiver for BOND_STATE_CHANGED actions.
IntentFilter intent = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// if the action is a change of the pairing state
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
// Get state and previous state.
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
final int prevState = intent.getIntExtra(
BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR);
if (state == BluetoothDevice.BOND_NONE &&
prevState == BluetoothDevice.BOND_BONDED) {
// The device has been successfully unpaired, inform the callback about this
callback.onDeviceUnpaired(device);
BluetoothHandler.this.context.unregisterReceiver(this);
} else if (state == BluetoothDevice.ERROR) {
callback.onUnpairingError(device);
BluetoothHandler.this.context.unregisterReceiver(this);
}
}
}
}, intent);
// Using reflection to invoke "removeBond" method in order to remove the pairing with
// a given device. This method is public in API lvl 18.
try {
Method method = device.getClass().getMethod("removeBond", (Class[]) null);
Object value = method.invoke(device, (Object[]) null);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public void startService() {
}
public void stopService() {
}
/**
* Callback interface for the process of pairing with a given device.
*/
public interface BluetoothDevicePairingCallback {
/**
* Called when the pairing process has been started.
*
* @param device the device to pair to.
*/
void onPairingStarted(BluetoothDevice device);
/**
* Called when the device has been successfully paired.
*
* @param device the successfully paired device.
*/
void onDevicePaired(BluetoothDevice device);
/**
* Called when the start of pairing has thrown an error (e.g., Bluetooth is disabled).
*
* @param device the device to which the pairing was intended.
*/
void onPairingError(BluetoothDevice device);
}
/**
* Callback interface for unpairing with a given device.
*/
public interface BluetoothDeviceUnpairingCallback {
/**
* Called when the device has been successfully unpaired.
*
* @param device the successfully unpaired device.
*/
void onDeviceUnpaired(BluetoothDevice device);
/**
* Called when the start of unpairing has thrown an error.
*
* @param device the device to unpair.
*/
void onUnpairingError(BluetoothDevice device);
}
/**
* Callback interface for the bluetooth discovery of other devices.
*/
public interface BluetoothDeviceDiscoveryCallback {
/**
* Called when the discovery has been started.
*/
void onActionDeviceDiscoveryStarted();
/**
* Called when the discovery has been finished.
*/
void onActionDeviceDiscoveryFinished();
/**
* Called when a new unpaired device has been discovered.
*
* @param device the newly discovered device that is not already paired.
*/
void onActionDeviceDiscovered(BluetoothDevice device);
}
}