/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program 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 version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.midp.jsr082.bluetooth;
import com.sun.kvem.jsr082.bluetooth.DiscoveryAgentImpl;
import com.sun.midp.jsr082.BluetoothUtils;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.RemoteDevice;
/**
* Represents native Bluetooth stack provided by the system.
* Provides services such as device and service discovery.
*/
public abstract class BluetoothStack {
/** Instance of BluetoothStack's subclass used in the current isolate. */
private static BluetoothStack instance = null;
/** Instance handle of the native porting layer class. */
private int nativeInstance = 0;
/** Listener where discovery events are reported to. */
private DiscoveryListener discListener = null;
/** Contains remote name request results. */
private Hashtable nameResults = new Hashtable();
/** Contains authentication request results. */
private Hashtable authenticateResults = new Hashtable();
/** Contains set encryption request results. */
private Hashtable encryptResults = new Hashtable();
/** Timeout value in milliseconds for friendly name retrieval. */
private final long ASK_FRIENDLY_NAME_TIMEOUT = 0;
/** Timeout value in milliseconds for authentication. */
private final long AUTHENTICATE_TIMEOUT = 0;
/** Timeout value in milliseconds for setting encryption. */
private final long ENCRYPT_TIMEOUT = 0;
/** Keeps the count of pending requests. */
int pollRequests = 0;
/**
* Contains results of ongoing inquiry to avoid reporting the same
* inquiry result twice.
*/
Vector inquiryHistory = new Vector();
/**
* Class constructor.
*/
protected BluetoothStack() {
initialize();
}
/**
* Allocates native resources.
*/
private native void initialize();
/**
* Releases native resources.
*/
private native void finalize();
/**
* Returns a BluetoothStack object.
*
* @return an instance of BluetoothStack subclass
*/
public synchronized static BluetoothStack getInstance() {
if (instance == null) {
// Note to porting engineer: please replace the class name with
// the one you intend to use on the target platform.
instance = new GenericBluetoothStack();
}
return instance;
}
/**
* Returns a BluetoothStack object and guarantees that Bluetooth
* radio is on.
* @return an instance of BluetoothStack subclass
* @throws BluetoothStateException if BluetoothStack is off and
* cannot be turned on.
*/
public synchronized static BluetoothStack getEnabledInstance()
throws BluetoothStateException {
getInstance();
if (!instance.isEnabled() && !instance.enable()) {
throw new BluetoothStateException("Failed turning Bluetooth on");
}
// intent here is launching EmulationPolling and SDPServer
// in emulation mode
com.sun.kvem.jsr082.bluetooth.SDDB.getInstance();
return instance;
}
/**
* Checks if the Bluetooth radio is enabled.
*
* @return true if Bluetooth is enabled, false otherwise
*/
public native boolean isEnabled();
/**
* Enables Bluetooth radio.
*
* @return true if Bluetooth is enabled, false otherwise
*/
public native boolean enable();
/**
* Returns Bluetooth address of the local device.
*
* @return Bluetooth address of the local device, or null if
* the address could not be retrieved
*/
public native String getLocalAddress();
/**
* Returns user-friendly name for the local device.
*
* @return User-friendly name for the local device, or null if
* the name could not be retrieved
*/
public native String getLocalName();
/**
* Returns class of device including service classes.
*
* @return class of device value, or -1 if the information could not
* be retrieved
*/
public native int getDeviceClass();
/**
* Sets major service class bits of the device.
*
* @param classes an integer whose binary representation indicates the major
* service class bits that should be set
* @return true if the operation succeeded, false otherwise
*/
public native boolean setServiceClasses(int classes);
/**
* Retrieves the inquiry access code that the local Bluetooth device is
* scanning for during inquiry scans.
*
* @return inquiry access code, or -1 if the information could not
* be retrieved
*/
public native int getAccessCode();
/**
* Sets the inquiry access code that the local Bluetooth device is
* scanning for during inquiry scans.
*
* @param accessCode inquiry access code to be set (valid values are in the
* range 0x9e8b00 to 0x9e8b3f), or 0 to take the device out of
* discoverable mode
* @return true if the operation succeeded, false otherwise
*/
public native boolean setAccessCode(int accessCode);
/**
* Places the device into inquiry mode.
*
* @param accessCode the type of inquiry
* @param listener the event listener that will receive discovery events
* @return true if the inquiry was started, false otherwise
*/
public boolean startInquiry(int accessCode, DiscoveryListener listener) {
if (discListener != null || listener == null) {
return false;
}
discListener = listener;
if (startInquiry(accessCode)) {
inquiryHistory.removeAllElements();
startPolling();
return true;
}
return false;
}
/**
* Removes the device from inquiry mode.
*
* @param listener the listener that is receiving inquiry events
* @return true if the inquiry was canceled, false otherwise
*/
public boolean cancelInquiry(DiscoveryListener listener) {
if (discListener != listener) {
return false;
}
if (cancelInquiry()) {
stopPolling();
discListener = null;
return true;
}
return false;
}
/**
* Retrieves friendly name from a remote device synchronously.
*
* @param addr remote device address
* @return friendly name of the remote device, or <code>null</code>
* if the name could not be retrieved
*/
public String askFriendlyNameSync(String addr) {
if (!askFriendlyName(addr)) {
return null;
}
nameResults.remove(addr);
startPolling();
return (String)waitResult(nameResults, addr,
ASK_FRIENDLY_NAME_TIMEOUT);
}
/**
* Performs remote device authentication synchronously.
*
* @param addr remote device address
* @return <code>true</code> if authentication was successful,
* <code>false</code> otherwise
*/
public boolean authenticateSync(String addr) {
if (!authenticate(addr)) {
return false;
}
int handle = getHandle(addr);
authenticateResults.remove(new Integer(handle));
startPolling();
Boolean result = (Boolean)waitResult(authenticateResults,
new Integer(handle), AUTHENTICATE_TIMEOUT);
if (result == null) {
return false;
}
return result.booleanValue();
}
/**
* Sets encryption mode synchronously.
*
* @param addr remote device address
* @param enable <code>true</code> if the encryption needs to be enabled,
* <code>false</code> otherwise
* @return <code>true</code> if authentication was successful,
* <code>false</code> otherwise
*/
public boolean encryptSync(String addr, boolean enable) {
if (!encrypt(addr, enable)) {
return false;
}
int handle = getHandle(addr);
encryptResults.remove(new Integer(handle));
startPolling();
Boolean result = (Boolean)waitResult(encryptResults,
new Integer(handle), ENCRYPT_TIMEOUT);
if (result == null) {
return false;
}
return result.booleanValue();
}
/**
* Starts a supplementary polling thread.
*/
public synchronized void startPolling() {
pollRequests++;
PollingThread.resume();
}
/**
* Cancels event polling for one request. Polling thread will continue to
* run unless there are no other pending requests.
*/
public synchronized void stopPolling() {
pollRequests--;
if (pollRequests > 0) {
return;
}
PollingThread.suspend();
}
/**
* Checks for Bluetooth events and processes them.
*/
public void pollEvents() {
while (checkEvents()) {
BluetoothEvent event = retrieveEvent();
if (event != null) {
event.dispatch();
}
}
}
/**
* Retrieves Bluetooth event.
*
* @return a Bluetooth event object
*/
protected abstract BluetoothEvent retrieveEvent();
/**
* Called when an inquiry request is completed.
*
* @param success indicates whether inquiry completed successfully
*/
void onInquiryComplete(boolean success) {
if (discListener == null) {
return;
}
stopPolling();
discListener = null;
inquiryHistory.removeAllElements();
int type = success ? DiscoveryListener.INQUIRY_COMPLETED :
DiscoveryListener.INQUIRY_ERROR;
DiscoveryAgentImpl.getInstance().inquiryCompleted(type);
}
/**
* Called when an inquiry result is obtained.
*
* @param result inquiry result object
*/
void onInquiryResult(InquiryResult result) {
if (discListener == null) {
return;
}
String addr = result.getAddress();
Enumeration e = inquiryHistory.elements();
while (e.hasMoreElements()) {
InquiryResult oldResult = (InquiryResult)e.nextElement();
if (oldResult.getAddress().equals(addr)) {
// inquiry result is already in our possession
return;
}
}
inquiryHistory.addElement(result);
RemoteDevice dev
= DiscoveryAgentImpl.getInstance().getRemoteDevice(addr);
DiscoveryAgentImpl.getInstance().addCachedDevice(addr);
discListener.deviceDiscovered(dev, result.getDeviceClass());
}
/**
* Called when a name retrieval request is completed.
*
* @param addr Bluetooth address of a remote device
* @param name friendly name of the device
*/
void onNameRetrieve(String addr, String name) {
stopPolling();
putResult(nameResults, addr, name);
}
/**
* Called when an authentication request is completed.
*
* @param handle connection handle for an ACL connection
* @param result indicates whether the operation was successful
*/
void onAuthenticationComplete(int handle, boolean result) {
stopPolling();
putResult(authenticateResults, new Integer(handle),
new Boolean(result));
}
/**
* Called when a set encryption request is completed.
*
* @param handle connection handle for an ACL connection
* @param result indicates whether the operation was successful
*/
void onEncryptionChange(int handle, boolean result) {
stopPolling();
putResult(encryptResults, new Integer(handle), new Boolean(result));
}
/**
* Puts result value into hastable and notifies threads waiting for the
* result to appear.
*
* @param hashtable <code>Hashtable</code> object where the result will be
* stored
* @param key key identifying the result
* @param value value of the result
*/
private void putResult(Hashtable hashtable, Object key, Object value) {
synchronized (hashtable) {
hashtable.put(key, value);
hashtable.notify();
}
}
/**
* Waits for the specified key to appear in the given hastable. If the key
* does not appear within the timeout specified, <code>null</code> value is
* returned.
*
* @param hashtable <code>Hashtable</code> object where the key is expected
* @param key the key expected to appear in the hastable
* @param timeout timeout value in milliseconds
* @return <code>Object</code> corresponding to the given key
*/
private Object waitResult(Hashtable hashtable, Object key, long timeout) {
synchronized (hashtable) {
if (timeout == 0) {
// infinite timeout
while (true) {
if (hashtable.containsKey(key)) {
return hashtable.remove(key);
}
try {
// wait for a new key-value pair to appear in hashtable
hashtable.wait();
} catch (InterruptedException e) {
return null;
}
}
}
// endTime indicates time up to which the method is allowed to run
long endTime = System.currentTimeMillis() + timeout;
while (true) {
if (hashtable.containsKey(key)) {
return hashtable.remove(key);
}
// update timeout value
timeout = endTime - System.currentTimeMillis();
if (timeout <= 0) {
return null;
}
try {
// wait for a new key-value pair to appear in hashtable
hashtable.wait(timeout);
} catch (InterruptedException e) {
return null;
}
}
}
}
/**
* Retrieves default ACL connection handle for the specified remote device.
*
* @param addr the Bluetooth address of the remote device
* @return ACL connection handle value
*/
private native int getHandle(String addr);
/**
* Passes device discovery request to the native porting layer.
*
* @param accessCode the type of inquiry
* @return <code>true</code> if the operation was accepted,
* <code>false</code> otherwise
*/
private native boolean startInquiry(int accessCode);
/**
* Passes cancellation of device discovery request to the native porting
* layer.
*
* @return <code>true</code> if the operation was accepted,
* <code>false</code> otherwise
*/
private native boolean cancelInquiry();
/**
* Passes remote device's friendly name acquisition request to the native
* porting layer.
*
* @param addr Bluetooth address of the remote device
* @return <code>true</code> if the operation was accepted,
* <code>false</code> otherwise
*/
private native boolean askFriendlyName(String addr);
/**
* Passes remote device authentication request to the native porting layer.
*
* @param addr Bluetooth address of the remote device
* @return <code>true</code> if the operation was accepted,
* <code>false</code> otherwise
*/
private native boolean authenticate(String addr);
/**
* Passes connection encryption change request to the native porting layer.
*
* @param addr Bluetooth address of the remote device
* @param enable <code>true</code> if the encryption needs to be enabled,
* <code>false</code> otherwise
* @return <code>true</code> if the operation was accepted,
* <code>false</code> otherwise
*/
private native boolean encrypt(String addr, boolean enable);
/**
* Checks if Bluetooth events are available on the native porting layer.
*
* @return <code>true</code> if there are pending events,
* <code>false</code> otherwise
*/
private native boolean checkEvents();
/**
* Reads binary event data from the native porting layer. This data can
* be interpreted differently by different subclasses of BluetoothStack.
*
* @param data byte array to be filled with data
* @return number of bytes read
*/
protected native int readData(byte[] data);
/**
* Creates Java String object from UTF-8 encoded string.
*
* @param buffer buffer containing the string in UTF-8 format
* @param offset offset of the first character in the buffer
* @param length length of the encoded string in bytes
* @return Java String object containing the string in UTF-16 format
*/
protected static native String stringUTF8(byte[] buffer, int offset,
int length);
}
/**
* Supplementary thread which periodically polls Bluetooth stack for events.
*/
class PollingThread extends Thread {
/** Instance of this class. */
private static PollingThread instance = new PollingThread();
/** Flag indicating if this thread should be suspended. */
private static boolean suspended = true;
/** Polling interval in milliseconds. */
private final int POLL_INTERVAL = 1000;
/**
* Class constructor.
*/
public PollingThread() {
}
/**
* Suspends this thread.
*/
public static void suspend() {
synchronized (instance) {
suspended = true;
}
}
/**
* Resumes this thread.
*/
public static void resume() {
try {
instance.start();
} catch (IllegalThreadStateException e) {
}
synchronized (instance) {
suspended = false;
instance.notify();
}
}
/**
* Execution body.
*/
public void run() {
BluetoothStack stack = BluetoothStack.getInstance();
while (true) {
try {
synchronized (this) {
if (suspended) {
wait();
}
}
stack.pollEvents();
synchronized (this) {
wait(POLL_INTERVAL);
}
} catch (InterruptedException e) {
break;
}
}
}
}
/**
* Supplementary thread which dispatches Bluetooth events to user notifiers.
*/
class Dispatcher extends Thread {
/** Instance of this class. */
private static Dispatcher instance = new Dispatcher();
/** Vector containing Bluetooth events awaiting to be dispatched. */
private static Vector events = new Vector();
/**
* Class constructor.
*/
public Dispatcher() {
}
/**
* Puts the event in the event queue.
* It also starts event dispatcher thread if it's not running yet.
*
* @param event the event to be enqueued
*/
public static void enqueue(BluetoothEvent event) {
try {
instance.start();
} catch (IllegalThreadStateException e) {
}
synchronized (events) {
events.addElement(event);
events.notify();
}
}
/**
* Execution body.
*/
public void run() {
while (true) {
BluetoothEvent event = null;
synchronized (events) {
if (!events.isEmpty()) {
event = (BluetoothEvent)events.firstElement();
events.removeElementAt(0);
} else {
try {
events.wait();
} catch (InterruptedException e) {
break;
}
}
}
if (event != null) {
event.process();
}
}
}
}
|