// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.BluetoothReflection;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.SdkLevel;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* BluetoothClient component
*
* @author lizlooney@google.com (Liz Looney)
*/
@DesignerComponent(version = YaVersion.BLUETOOTHCLIENT_COMPONENT_VERSION,
description = "Bluetooth client component",
category = ComponentCategory.CONNECTIVITY,
nonVisible = true,
iconName = "images/bluetooth.png")
@SimpleObject
@UsesPermissions(permissionNames =
"android.permission.BLUETOOTH, " +
"android.permission.BLUETOOTH_ADMIN")
public final class BluetoothClient extends BluetoothConnectionBase {
private static final String SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB";
private final List<Component> attachedComponents = new ArrayList<Component>();
private Set<Integer> acceptableDeviceClasses;
/**
* Creates a new BluetoothClient.
*/
public BluetoothClient(ComponentContainer container) {
super(container, "BluetoothClient");
}
boolean attachComponent(Component component, Set<Integer> acceptableDeviceClasses) {
if (attachedComponents.isEmpty()) {
// If this is the first/only attached component, we keep the acceptableDeviceClasses.
this.acceptableDeviceClasses = (acceptableDeviceClasses == null)
? null
: new HashSet<Integer>(acceptableDeviceClasses);
} else {
// If there is already one or more attached components, the acceptableDeviceClasses must be
// the same as what we already have.
if (this.acceptableDeviceClasses == null) {
if (acceptableDeviceClasses != null) {
return false;
}
} else {
if (acceptableDeviceClasses == null) {
return false;
}
if (!this.acceptableDeviceClasses.containsAll(acceptableDeviceClasses)) {
return false;
}
if (!acceptableDeviceClasses.containsAll(this.acceptableDeviceClasses)) {
return false;
}
}
}
attachedComponents.add(component);
return true;
}
void detachComponent(Component component) {
attachedComponents.remove(component);
if (attachedComponents.isEmpty()) {
acceptableDeviceClasses = null;
}
}
/**
* Checks whether the Bluetooth device with the given address is paired.
*
* @param address the MAC address of the Bluetooth device
* @return true if the device is paired, false otherwise
*/
@SimpleFunction(description = "Checks whether the Bluetooth device with the specified address " +
"is paired.")
public boolean IsDevicePaired(String address) {
String functionName = "IsDevicePaired";
Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter == null) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_AVAILABLE);
return false;
}
if (!BluetoothReflection.isBluetoothEnabled(bluetoothAdapter)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_ENABLED);
return false;
}
// Truncate the address at the first space.
// This allows the address to be an element from the AddressesAndNames property.
int firstSpace = address.indexOf(" ");
if (firstSpace != -1) {
address = address.substring(0, firstSpace);
}
if (!BluetoothReflection.checkBluetoothAddress(bluetoothAdapter, address)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_INVALID_ADDRESS);
return false;
}
Object bluetoothDevice = BluetoothReflection.getRemoteDevice(bluetoothAdapter, address);
return BluetoothReflection.isBonded(bluetoothDevice);
}
/**
* Returns the list of paired Bluetooth devices. Each element of the returned
* list is a String consisting of the device's address, a space, and the
* device's name.
*
* This method calls isDeviceClassAcceptable to determine whether to include
* a particular device in the returned list.
*
* @return a List representing the addresses and names of paired
* Bluetooth devices
*/
@SimpleProperty(description = "The addresses and names of paired Bluetooth devices",
category = PropertyCategory.BEHAVIOR)
public List<String> AddressesAndNames() {
List<String> addressesAndNames = new ArrayList<String>();
Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter != null) {
if (BluetoothReflection.isBluetoothEnabled(bluetoothAdapter)) {
for (Object bluetoothDevice : BluetoothReflection.getBondedDevices(bluetoothAdapter)) {
if (isDeviceClassAcceptable(bluetoothDevice)) {
String name = BluetoothReflection.getBluetoothDeviceName(bluetoothDevice);
String address = BluetoothReflection.getBluetoothDeviceAddress(bluetoothDevice);
addressesAndNames.add(address + " " + name);
}
}
}
}
return addressesAndNames;
}
/**
* Returns true if the class of the given device is acceptable.
*
* @param bluetoothDevice the Bluetooth device
*/
private boolean isDeviceClassAcceptable(Object bluetoothDevice) {
if (acceptableDeviceClasses == null) {
// Add devices are acceptable.
return true;
}
Object bluetoothClass = BluetoothReflection.getBluetoothClass(bluetoothDevice);
if (bluetoothClass == null) {
// This device has no class.
return false;
}
int deviceClass = BluetoothReflection.getDeviceClass(bluetoothClass);
return acceptableDeviceClasses.contains(deviceClass);
}
/**
* Connect to a Bluetooth device with the given address.
*
* @param address the MAC address of the Bluetooth device
* @return true if the connection was successful, false otherwise
*/
@SimpleFunction(description = "Connect to the Bluetooth device with the specified address and " +
"the Serial Port Profile (SPP). Returns true if the connection was successful.")
public boolean Connect(String address) {
return connect("Connect", address, SPP_UUID);
}
/**
* Connect to a Bluetooth device with the given address and a specific UUID.
*
* @param address the MAC address of the Bluetooth device
* @param uuid the UUID
* @return true if the connection was successful, false otherwise
*/
@SimpleFunction(description = "Connect to the Bluetooth device with the specified address and " +
"UUID. Returns true if the connection was successful.")
public boolean ConnectWithUUID(String address, String uuid) {
return connect("ConnectWithUUID", address, uuid);
}
/**
* Connects to a Bluetooth device with the given address and UUID.
*
* If the address contains a space, the space and any characters after it
* are ignored. This facilitates passing an element of the list returned from
* the addressesAndNames method above.
*
* @param functionName the name of the SimpleFunction calling this method
* @param address the address of the device
* @param uuidString the UUID
*/
private boolean connect(String functionName, String address, String uuidString) {
Object bluetoothAdapter = BluetoothReflection.getBluetoothAdapter();
if (bluetoothAdapter == null) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_AVAILABLE);
return false;
}
if (!BluetoothReflection.isBluetoothEnabled(bluetoothAdapter)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_ENABLED);
return false;
}
// Truncate the address at the first space.
// This allows the address to be an element from the AddressesAndNames property.
int firstSpace = address.indexOf(" ");
if (firstSpace != -1) {
address = address.substring(0, firstSpace);
}
if (!BluetoothReflection.checkBluetoothAddress(bluetoothAdapter, address)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_INVALID_ADDRESS);
return false;
}
Object bluetoothDevice = BluetoothReflection.getRemoteDevice(bluetoothAdapter, address);
if (!BluetoothReflection.isBonded(bluetoothDevice)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_PAIRED_DEVICE);
return false;
}
if (!isDeviceClassAcceptable(bluetoothDevice)) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_NOT_REQUIRED_CLASS_OF_DEVICE);
return false;
}
UUID uuid;
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_INVALID_UUID, uuidString);
return false;
}
Disconnect();
try {
connect(bluetoothDevice, uuid);
return true;
} catch (IOException e) {
Disconnect();
form.dispatchErrorOccurredEvent(this, functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_CONNECT);
return false;
}
}
private void connect(Object bluetoothDevice, UUID uuid) throws IOException {
Object bluetoothSocket;
if (!secure && SdkLevel.getLevel() >= SdkLevel.LEVEL_GINGERBREAD_MR1) {
// createInsecureRfcommSocketToServiceRecord was introduced in level 10
bluetoothSocket = BluetoothReflection.createInsecureRfcommSocketToServiceRecord(
bluetoothDevice, uuid);
} else {
bluetoothSocket = BluetoothReflection.createRfcommSocketToServiceRecord(
bluetoothDevice, uuid);
}
BluetoothReflection.connectToBluetoothSocket(bluetoothSocket);
setConnection(bluetoothSocket);
Log.i(logTag, "Connected to Bluetooth device " +
BluetoothReflection.getBluetoothDeviceAddress(bluetoothDevice) + " " +
BluetoothReflection.getBluetoothDeviceName(bluetoothDevice) + ".");
}
}