diff options
Diffstat (limited to 'core/java/android/bluetooth')
17 files changed, 3397 insertions, 0 deletions
diff --git a/core/java/android/bluetooth/AtCommandHandler.java b/core/java/android/bluetooth/AtCommandHandler.java new file mode 100644 index 0000000..8de2133 --- /dev/null +++ b/core/java/android/bluetooth/AtCommandHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.AtCommandResult; + +/** + * Handler Interface for {@link AtParser}.<p> + * @hide + */ +public abstract class AtCommandHandler { + + /** + * Handle Basic commands "ATA".<p> + * These are single letter commands such as ATA and ATD. Anything following + * the single letter command ('A' and 'D' respectively) will be passed as + * 'arg'.<p> + * For example, "ATDT1234" would result in the call + * handleBasicCommand("T1234").<p> + * @param arg Everything following the basic command character. + * @return The result of this command. + */ + public AtCommandResult handleBasicCommand(String arg) { + return new AtCommandResult(AtCommandResult.ERROR); + } + + /** + * Handle Actions command "AT+FOO".<p> + * Action commands are part of the Extended command syntax, and are + * typically used to signal an action on "FOO".<p> + * @return The result of this command. + */ + public AtCommandResult handleActionCommand() { + return new AtCommandResult(AtCommandResult.ERROR); + } + + /** + * Handle Read command "AT+FOO?".<p> + * Read commands are part of the Extended command syntax, and are + * typically used to read the value of "FOO".<p> + * @return The result of this command. + */ + public AtCommandResult handleReadCommand() { + return new AtCommandResult(AtCommandResult.ERROR); + } + + /** + * Handle Set command "AT+FOO=...".<p> + * Set commands are part of the Extended command syntax, and are + * typically used to set the value of "FOO". Multiple arguments can be + * sent.<p> + * AT+FOO=[<arg1>[,<arg2>[,...]]]<p> + * Each argument will be either numeric (Integer) or String. + * handleSetCommand is passed a generic Object[] array in which each + * element will be an Integer (if it can be parsed with parseInt()) or + * String.<p> + * Missing arguments ",," are set to empty Strings.<p> + * @param args Array of String and/or Integer's. There will always be at + * least one element in this array. + * @return The result of this command. + */ + // Typically used to set this paramter + public AtCommandResult handleSetCommand(Object[] args) { + return new AtCommandResult(AtCommandResult.ERROR); + } + + /** + * Handle Test command "AT+FOO=?".<p> + * Test commands are part of the Extended command syntax, and are typically + * used to request an indication of the range of legal values that "FOO" + * can take.<p> + * By defualt we return an OK result, to indicate that this command is at + * least recognized.<p> + * @return The result of this command. + */ + public AtCommandResult handleTestCommand() { + return new AtCommandResult(AtCommandResult.OK); + } +} diff --git a/core/java/android/bluetooth/AtCommandResult.java b/core/java/android/bluetooth/AtCommandResult.java new file mode 100644 index 0000000..638be2d --- /dev/null +++ b/core/java/android/bluetooth/AtCommandResult.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import java.util.*; + +/** + * The result of execution of an single AT command.<p> + * + * + * This class can represent the final response to an AT command line, and also + * intermediate responses to a single command within a chained AT command + * line.<p> + * + * The actual responses that are intended to be send in reply to the AT command + * line are stored in a string array. The final response is stored as an + * int enum, converted to a string when toString() is called. Only a single + * final response is sent from multiple commands chained into a single command + * line.<p> + * @hide + */ +public class AtCommandResult { + // Result code enumerations + public static final int OK = 0; + public static final int ERROR = 1; + public static final int UNSOLICITED = 2; + + private static final String OK_STRING = "OK"; + private static final String ERROR_STRING = "ERROR"; + + private int mResultCode; // Result code + private StringBuilder mResponse; // Response with CRLF line breaks + + /** + * Construct a new AtCommandResult with given result code, and an empty + * response array. + * @param resultCode One of OK, ERROR or UNSOLICITED. + */ + public AtCommandResult(int resultCode) { + mResultCode = resultCode; + mResponse = new StringBuilder(); + } + + /** + * Construct a new AtCommandResult with result code OK, and the specified + * single line response. + * @param response The single line response. + */ + public AtCommandResult(String response) { + this(OK); + addResponse(response); + } + + public int getResultCode() { + return mResultCode; + } + + /** + * Add another line to the response. + */ + public void addResponse(String response) { + appendWithCrlf(mResponse, response); + } + + /** + * Add the given result into this AtCommandResult object.<p> + * Used to combine results from multiple commands in a single command line + * (command chaining). + * @param result The AtCommandResult to add to this result. + */ + public void addResult(AtCommandResult result) { + if (result != null) { + appendWithCrlf(mResponse, result.mResponse.toString()); + mResultCode = result.mResultCode; + } + } + + /** + * Generate the string response ready to send + */ + public String toString() { + StringBuilder result = new StringBuilder(mResponse.toString()); + switch (mResultCode) { + case OK: + appendWithCrlf(result, OK_STRING); + break; + case ERROR: + appendWithCrlf(result, ERROR_STRING); + break; + } + return result.toString(); + } + + /** Append a string to a string builder, joining with a double + * CRLF. Used to create multi-line AT command replies + */ + public static void appendWithCrlf(StringBuilder str1, String str2) { + if (str1.length() > 0 && str2.length() > 0) { + str1.append("\r\n\r\n"); + } + str1.append(str2); + } +}; diff --git a/core/java/android/bluetooth/AtParser.java b/core/java/android/bluetooth/AtParser.java new file mode 100644 index 0000000..1ea3150 --- /dev/null +++ b/core/java/android/bluetooth/AtParser.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.AtCommandHandler; +import android.bluetooth.AtCommandResult; + +import java.util.*; + +/** + * An AT (Hayes command) Parser based on (a subset of) the ITU-T V.250 standard. + * <p> + * + * Conforment with the subset of V.250 required for implementation of the + * Bluetooth Headset and Handsfree Profiles, as per Bluetooth SIP + * specifications. Also implements some V.250 features not required by + * Bluetooth - such as chained commands.<p> + * + * Command handlers are registered with an AtParser object. These handlers are + * invoked when command lines are processed by AtParser's process() method.<p> + * + * The AtParser object accepts a new command line to parse via its process() + * method. It breaks each command line into one or more commands. Each command + * is parsed for name, type, and (optional) arguments, and an appropriate + * external handler method is called through the AtCommandHandler interface. + * + * The command types are<ul> + * <li>Basic Command. For example "ATDT1234567890". Basic command names are a + * single character (e.g. "D"), and everything following this character is + * passed to the handler as a string argument (e.g. "T1234567890"). + * <li>Action Command. For example "AT+CIMI". The command name is "CIMI", and + * there are no arguments for action commands. + * <li>Read Command. For example "AT+VGM?". The command name is "VGM", and there + * are no arguments for get commands. + * <li>Set Command. For example "AT+VGM=14". The command name is "VGM", and + * there is a single integer argument in this case. In the general case then + * can be zero or more arguments (comma deliminated) each of integer or string + * form. + * <li>Test Command. For example "AT+VGM=?. No arguments. + * </ul> + * + * In V.250 the last four command types are known as Extended Commands, and + * they are used heavily in Bluetooth.<p> + * + * Basic commands cannot be chained in this implementation. For Bluetooth + * headset/handsfree use this is acceptable, because they only use the basic + * commands ATA and ATD, which are not allowed to be chained. For general V.250 + * use we would need to improve this class to allow Basic command chaining - + * however its tricky to get right becuase there is no deliminator for Basic + * command chaining.<p> + * + * Extended commands can be chained. For example:<p> + * AT+VGM?;+VGM=14;+CIMI<p> + * This is equivalent to:<p> + * AT+VGM? + * AT+VGM=14 + * AT+CIMI + * Except that only one final result code is return (although several + * intermediate responses may be returned), and as soon as one command in the + * chain fails the rest are abandonded.<p> + * + * Handlers are registered by there command name via register(Char c, ...) or + * register(String s, ...). Handlers for Basic command should be registered by + * the basic command character, and handlers for Extended commands should be + * registered by String.<p> + * + * Refer to:<ul> + * <li>ITU-T Recommendation V.250 + * <li>ETSI TS 127.007 (AT Comannd set for User Equipment, 3GPP TS 27.007) + * <li>Bluetooth Headset Profile Spec (K6) + * <li>Bluetooth Handsfree Profile Spec (HFP 1.5) + * </ul> + * @hide + */ +public class AtParser { + + // Extended command type enumeration, only used internally + private static final int TYPE_ACTION = 0; // AT+FOO + private static final int TYPE_READ = 1; // AT+FOO? + private static final int TYPE_SET = 2; // AT+FOO= + private static final int TYPE_TEST = 3; // AT+FOO=? + + private HashMap<String, AtCommandHandler> mExtHandlers; + private HashMap<Character, AtCommandHandler> mBasicHandlers; + + private String mLastInput; // for "A/" (repeat last command) support + + /** + * Create a new AtParser.<p> + * No handlers are registered. + */ + public AtParser() { + mBasicHandlers = new HashMap<Character, AtCommandHandler>(); + mExtHandlers = new HashMap<String, AtCommandHandler>(); + mLastInput = ""; + } + + /** + * Register a Basic command handler.<p> + * Basic command handlers are later called via their + * <code>handleBasicCommand(String args)</code> method. + * @param command Command name - a single character + * @param handler Handler to register + */ + public void register(Character command, AtCommandHandler handler) { + mBasicHandlers.put(command, handler); + } + + /** + * Register an Extended command handler.<p> + * Extended command handlers are later called via:<ul> + * <li><code>handleActionCommand()</code> + * <li><code>handleGetCommand()</code> + * <li><code>handleSetCommand()</code> + * <li><code>handleTestCommand()</code> + * </ul> + * Only one method will be called for each command processed. + * @param command Command name - can be multiple characters + * @param handler Handler to register + */ + public void register(String command, AtCommandHandler handler) { + mExtHandlers.put(command, handler); + } + + + /** + * Strip input of whitespace and force Uppercase - except sections inside + * quotes. Also fixes unmatched quotes (by appending a quote). Double + * quotes " are the only quotes allowed by V.250 + */ + static private String clean(String input) { + StringBuilder out = new StringBuilder(input.length()); + + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == '"') { + int j = input.indexOf('"', i + 1 ); // search for closing " + if (j == -1) { // unmatched ", insert one. + out.append(input.substring(i, input.length())); + out.append('"'); + break; + } + out.append(input.substring(i, j + 1)); + i = j; + } else if (c != ' ') { + out.append(Character.toUpperCase(c)); + } + } + + return out.toString(); + } + + static private boolean isAtoZ(char c) { + return (c >= 'A' && c <= 'Z'); + } + + /** + * Find a character ch, ignoring quoted sections. + * Return input.length() if not found. + */ + static private int findChar(char ch, String input, int fromIndex) { + for (int i = fromIndex; i < input.length(); i++) { + char c = input.charAt(i); + if (c == '"') { + i = input.indexOf('"', i + 1); + if (i == -1) { + return input.length(); + } + } else if (c == ch) { + return i; + } + } + return input.length(); + } + + /** + * Break an argument string into individual arguments (comma deliminated). + * Integer arguments are turned into Integer objects. Otherwise a String + * object is used. + */ + static private Object[] generateArgs(String input) { + int i = 0; + int j; + ArrayList<Object> out = new ArrayList<Object>(); + while (i <= input.length()) { + j = findChar(',', input, i); + + String arg = input.substring(i, j); + try { + out.add(new Integer(arg)); + } catch (NumberFormatException e) { + out.add(arg); + } + + i = j + 1; // move past comma + } + return out.toArray(); + } + + /** + * Return the index of the end of character after the last characeter in + * the extended command name. Uses the V.250 spec for allowed command + * names. + */ + static private int findEndExtendedName(String input, int index) { + for (int i = index; i < input.length(); i++) { + char c = input.charAt(i); + + // V.250 defines the following chars as legal extended command + // names + if (isAtoZ(c)) continue; + if (c >= '0' && c <= '9') continue; + switch (c) { + case '!': + case '%': + case '-': + case '.': + case '/': + case ':': + case '_': + continue; + default: + return i; + } + } + return input.length(); + } + + /** + * Processes an incoming AT command line.<p> + * This method will invoke zero or one command handler methods for each + * command in the command line.<p> + * @param raw_input The AT input, without EOL deliminator (e.g. <CR>). + * @return Result object for this command line. This can be + * converted to a String[] response with toStrings(). + */ + public AtCommandResult process(String raw_input) { + String input = clean(raw_input); + + // Handle "A/" (repeat previous line) + if (input.regionMatches(0, "A/", 0, 2)) { + input = new String(mLastInput); + } else { + mLastInput = new String(input); + } + + // Handle empty line - no response necessary + if (input.equals("")) { + // Return [] + return new AtCommandResult(AtCommandResult.UNSOLICITED); + } + + // Anything else deserves an error + if (!input.regionMatches(0, "AT", 0, 2)) { + // Return ["ERROR"] + return new AtCommandResult(AtCommandResult.ERROR); + } + + // Ok we have a command that starts with AT. Process it + int index = 2; + AtCommandResult result = + new AtCommandResult(AtCommandResult.UNSOLICITED); + while (index < input.length()) { + char c = input.charAt(index); + + if (isAtoZ(c)) { + // Option 1: Basic Command + // Pass the rest of the line as is to the handler. Do not + // look for any more commands on this line. + String args = input.substring(index + 1); + if (mBasicHandlers.containsKey((Character)c)) { + result.addResult(mBasicHandlers.get( + (Character)c).handleBasicCommand(args)); + return result; + } else { + // no handler + result.addResult( + new AtCommandResult(AtCommandResult.ERROR)); + return result; + } + // control never reaches here + } + + if (c == '+') { + // Option 2: Extended Command + // Search for first non-name character. Shortcircuit if we dont + // handle this command name. + int i = findEndExtendedName(input, index + 1); + String commandName = input.substring(index, i); + if (!mExtHandlers.containsKey(commandName)) { + // no handler + result.addResult( + new AtCommandResult(AtCommandResult.ERROR)); + return result; + } + AtCommandHandler handler = mExtHandlers.get(commandName); + + // Search for end of this command - this is usually the end of + // line + int endIndex = findChar(';', input, index); + + // Determine what type of command this is. + // Default to TYPE_ACTION if we can't find anything else + // obvious. + int type; + + if (i >= endIndex) { + type = TYPE_ACTION; + } else if (input.charAt(i) == '?') { + type = TYPE_READ; + } else if (input.charAt(i) == '=') { + if (i + 1 < endIndex) { + if (input.charAt(i + 1) == '?') { + type = TYPE_TEST; + } else { + type = TYPE_SET; + } + } else { + type = TYPE_SET; + } + } else { + type = TYPE_ACTION; + } + + // Call this command. Short-circuit as soon as a command fails + switch (type) { + case TYPE_ACTION: + result.addResult(handler.handleActionCommand()); + break; + case TYPE_READ: + result.addResult(handler.handleReadCommand()); + break; + case TYPE_TEST: + result.addResult(handler.handleTestCommand()); + break; + case TYPE_SET: + Object[] args = + generateArgs(input.substring(i + 1, endIndex)); + result.addResult(handler.handleSetCommand(args)); + break; + } + if (result.getResultCode() != AtCommandResult.OK) { + return result; // short-circuit + } + + index = endIndex; + } else { + // Can't tell if this is a basic or extended command. + // Push forwards and hope we hit something. + index++; + } + } + // Finished processing (and all results were ok) + return result; + } +} diff --git a/core/java/android/bluetooth/BluetoothAudioGateway.java b/core/java/android/bluetooth/BluetoothAudioGateway.java new file mode 100644 index 0000000..f3afd2a --- /dev/null +++ b/core/java/android/bluetooth/BluetoothAudioGateway.java @@ -0,0 +1,190 @@ +package android.bluetooth; + +import java.lang.Thread; + +import android.os.Message; +import android.os.Handler; +import android.util.Log; + +/** + * Listen's for incoming RFCOMM connection for the headset / handsfree service. + * + * This class is planned for deletion, in favor of a generic Rfcomm class. + * + * @hide + */ +public class BluetoothAudioGateway { + private static final String TAG = "BT Audio Gateway"; + private static final boolean DBG = false; + + private int mNativeData; + static { classInitNative(); } + + private BluetoothDevice mBluetooth; + + /* in */ + private int mHandsfreeAgRfcommChannel = -1; + private int mHeadsetAgRfcommChannel = -1; + + /* out */ + private String mConnectingHeadsetAddress; + private int mConnectingHeadsetRfcommChannel; /* -1 when not connected */ + private int mConnectingHeadsetSocketFd; + private String mConnectingHandsfreeAddress; + private int mConnectingHandsfreeRfcommChannel; /* -1 when not connected */ + private int mConnectingHandsfreeSocketFd; + private int mTimeoutRemainingMs; /* in/out */ + + public static final int DEFAULT_HF_AG_CHANNEL = 10; + public static final int DEFAULT_HS_AG_CHANNEL = 11; + + public BluetoothAudioGateway(BluetoothDevice bluetooth) { + this(bluetooth, DEFAULT_HF_AG_CHANNEL, DEFAULT_HS_AG_CHANNEL); + } + + public BluetoothAudioGateway(BluetoothDevice bluetooth, + int handsfreeAgRfcommChannel, + int headsetAgRfcommChannel) { + mBluetooth = bluetooth; + mHandsfreeAgRfcommChannel = handsfreeAgRfcommChannel; + mHeadsetAgRfcommChannel = headsetAgRfcommChannel; + initializeNativeDataNative(); + } + + private Thread mConnectThead; + private volatile boolean mInterrupted; + private static final int SELECT_WAIT_TIMEOUT = 1000; + + private Handler mCallback; + + public class IncomingConnectionInfo { + IncomingConnectionInfo(BluetoothDevice bluetooth, String address, int socketFd, + int rfcommChan) { + mBluetooth = bluetooth; + mAddress = address; + mSocketFd = socketFd; + mRfcommChan = rfcommChan; + } + + public BluetoothDevice mBluetooth; + public String mAddress; + public int mSocketFd; + public int mRfcommChan; + } + + public static final int MSG_INCOMING_HEADSET_CONNECTION = 100; + public static final int MSG_INCOMING_HANDSFREE_CONNECTION = 101; + + public synchronized boolean start(Handler callback) { + + if (mConnectThead == null) { + mCallback = callback; + mConnectThead = new Thread(TAG) { + public void run() { + if (DBG) log("Connect Thread starting"); + while (!mInterrupted) { + //Log.i(TAG, "waiting for connect"); + mConnectingHeadsetRfcommChannel = -1; + mConnectingHandsfreeRfcommChannel = -1; + if (waitForHandsfreeConnectNative(SELECT_WAIT_TIMEOUT) == false) { + if (mTimeoutRemainingMs > 0) { + try { + Log.i(TAG, "select thread timed out, but " + + mTimeoutRemainingMs + "ms of waiting remain."); + Thread.sleep(mTimeoutRemainingMs); + } catch (InterruptedException e) { + Log.i(TAG, "select thread was interrupted (2), exiting"); + mInterrupted = true; + } + } + } + else { + Log.i(TAG, "connect notification!"); + /* A device connected (most likely just one, but + it is possible for two separate devices, one + a headset and one a handsfree, to connect + simultaneously. + */ + if (mConnectingHeadsetRfcommChannel >= 0) { + Log.i(TAG, "Incoming connection from headset " + + mConnectingHeadsetAddress + " on channel " + + mConnectingHeadsetRfcommChannel); + Message msg = Message.obtain(mCallback); + msg.what = MSG_INCOMING_HEADSET_CONNECTION; + msg.obj = + new IncomingConnectionInfo( + mBluetooth, + mConnectingHeadsetAddress, + mConnectingHeadsetSocketFd, + mConnectingHeadsetRfcommChannel); + msg.sendToTarget(); + } + if (mConnectingHandsfreeRfcommChannel >= 0) { + Log.i(TAG, "Incoming connection from handsfree " + + mConnectingHandsfreeAddress + " on channel " + + mConnectingHandsfreeRfcommChannel); + Message msg = Message.obtain(); + msg.setTarget(mCallback); + msg.what = MSG_INCOMING_HANDSFREE_CONNECTION; + msg.obj = + new IncomingConnectionInfo( + mBluetooth, + mConnectingHandsfreeAddress, + mConnectingHandsfreeSocketFd, + mConnectingHandsfreeRfcommChannel); + msg.sendToTarget(); + } + } + } + if (DBG) log("Connect Thread finished"); + } + }; + + if (setUpListeningSocketsNative() == false) { + Log.e(TAG, "Could not set up listening socket, exiting"); + return false; + } + + mInterrupted = false; + mConnectThead.start(); + } + + return true; + } + + public synchronized void stop() { + if (mConnectThead != null) { + if (DBG) log("stopping Connect Thread"); + mInterrupted = true; + try { + mConnectThead.interrupt(); + if (DBG) log("waiting for thread to terminate"); + mConnectThead.join(); + mConnectThead = null; + mCallback = null; + tearDownListeningSocketsNative(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted waiting for Connect Thread to join"); + } + } + } + + protected void finalize() throws Throwable { + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + + private static native void classInitNative(); + private native void initializeNativeDataNative(); + private native void cleanupNativeDataNative(); + private native boolean waitForHandsfreeConnectNative(int timeoutMs); + private native boolean setUpListeningSocketsNative(); + private native void tearDownListeningSocketsNative(); + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java new file mode 100644 index 0000000..0b24db6 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.os.RemoteException; +import android.util.Log; + +import java.io.UnsupportedEncodingException; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Manages the local Bluetooth device. Scan for devices, create bondings, + * power up and down the adapter. + * + * @hide + */ +public class BluetoothDevice { + public static final int MODE_UNKNOWN = -1; + public static final int MODE_OFF = 0; + public static final int MODE_CONNECTABLE = 1; + public static final int MODE_DISCOVERABLE = 2; + + public static final int RESULT_FAILURE = -1; + public static final int RESULT_SUCCESS = 0; + + private static final String TAG = "BluetoothDevice"; + + private final IBluetoothDevice mService; + /** + * @hide - hide this because it takes a parameter of type + * IBluetoothDevice, which is a System private class. + * Also note that Context.getSystemService is a factory that + * returns a BlueToothDevice. That is the right way to get + * a BluetoothDevice. + */ + public BluetoothDevice(IBluetoothDevice service) { + mService = service; + } + + /** + * Get the current status of Bluetooth hardware. + * + * @return true if Bluetooth enabled, false otherwise. + */ + public boolean isEnabled() { + try { + return mService.isEnabled(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Enable the Bluetooth device. + * Turn on the underlying hardware. + * This is an asynchronous call, BluetoothIntent.ENABLED_ACTION will be + * sent if and when the device is successfully enabled. + * @return false if we cannot enable the Bluetooth device. True does not + * imply the device was enabled, it only implies that so far there were no + * problems. + */ + public boolean enable() { + return enable(null); + } + + /** + * Enable the Bluetooth device. + * Turns on the underlying hardware. + * This is an asynchronous call. onEnableResult() of your callback will be + * called when the call is complete, with either RESULT_SUCCESS or + * RESULT_FAILURE. + * + * Your callback will be called from a binder thread, not the main thread. + * + * In addition to the callback, BluetoothIntent.ENABLED_ACTION will be + * broadcast if the device is successfully enabled. + * + * @param callback Your callback, null is ok. + * @return true if your callback was successfully registered, or false if + * there was an error, implying your callback will never be called. + */ + public boolean enable(IBluetoothDeviceCallback callback) { + try { + return mService.enable(callback); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Disable the Bluetooth device. + * This turns off the underlying hardware. + * + * @return true if successful, false otherwise. + */ + public boolean disable() { + try { + return mService.disable(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public String getAddress() { + try { + return mService.getAddress(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Get the friendly Bluetooth name of this device. + * + * This name is visible to remote Bluetooth devices. Currently it is only + * possible to retrieve the Bluetooth name when Bluetooth is enabled. + * + * @return the Bluetooth name, or null if there was a problem. + */ + public String getName() { + try { + return mService.getName(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Set the friendly Bluetooth name of this device. + * + * This name is visible to remote Bluetooth devices. The Bluetooth Service + * is responsible for persisting this name. + * + * @param name the name to set + * @return true, if the name was successfully set. False otherwise. + */ + public boolean setName(String name) { + try { + return mService.setName(name); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public String getMajorClass() { + try { + return mService.getMajorClass(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getMinorClass() { + try { + return mService.getMinorClass(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getVersion() { + try { + return mService.getVersion(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRevision() { + try { + return mService.getRevision(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getManufacturer() { + try { + return mService.getManufacturer(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getCompany() { + try { + return mService.getCompany(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + public int getMode() { + try { + return mService.getMode(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return MODE_UNKNOWN; + } + public void setMode(int mode) { + try { + mService.setMode(mode); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + public int getDiscoverableTimeout() { + try { + return mService.getDiscoverableTimeout(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return -1; + } + public void setDiscoverableTimeout(int timeout) { + try { + mService.setDiscoverableTimeout(timeout); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + public boolean startDiscovery() { + return startDiscovery(true); + } + public boolean startDiscovery(boolean resolveNames) { + try { + return mService.startDiscovery(resolveNames); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public void cancelDiscovery() { + try { + mService.cancelDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + public boolean isDiscovering() { + try { + return mService.isDiscovering(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public boolean startPeriodicDiscovery() { + try { + return mService.startPeriodicDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + public boolean stopPeriodicDiscovery() { + try { + return mService.stopPeriodicDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + public boolean isPeriodicDiscovery() { + try { + return mService.isPeriodicDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public String[] listRemoteDevices() { + try { + return mService.listRemoteDevices(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * List remote devices that have a low level (ACL) connection. + * + * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have + * an ACL connection even when not paired - this is common for SDP queries + * or for in-progress pairing requests. + * + * In most cases you probably want to test if a higher level protocol is + * connected, rather than testing ACL connections. + * + * @return bluetooth hardware addresses of remote devices with a current + * ACL connection. Array size is 0 if no devices have a + * connection. Null on error. + */ + public String[] listAclConnections() { + try { + return mService.listAclConnections(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Check if a specified remote device has a low level (ACL) connection. + * + * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have + * an ACL connection even when not paired - this is common for SDP queries + * or for in-progress pairing requests. + * + * In most cases you probably want to test if a higher level protocol is + * connected, rather than testing ACL connections. + * + * @param address the Bluetooth hardware address you want to check. + * @return true if there is an ACL connection, false otherwise and on + * error. + */ + public boolean isAclConnected(String address) { + try { + return mService.isAclConnected(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Perform a low level (ACL) disconnection of a remote device. + * + * This forcably disconnects the ACL layer connection to a remote device, + * which will cause all RFCOMM, SDP and L2CAP connections to this remote + * device to close. + * + * @param address the Bluetooth hardware address you want to disconnect. + * @return true if the device was disconnected, false otherwise and on + * error. + */ + public boolean disconnectRemoteDeviceAcl(String address) { + try { + return mService.disconnectRemoteDeviceAcl(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Create a bonding with a remote bluetooth device. + * + * This is an asynchronous call. BluetoothIntent.BONDING_CREATED_ACTION + * will be broadcast if and when the remote device is successfully bonded. + * + * @param address the remote device Bluetooth address. + * @return false if we cannot create a bonding to that device, true if + * there were no problems beginning the bonding process. + */ + public boolean createBonding(String address) { + return createBonding(address, null); + } + + /** + * Create a bonding with a remote bluetooth device. + * + * This is an asynchronous call. onCreateBondingResult() of your callback + * will be called when the call is complete, with either RESULT_SUCCESS or + * RESULT_FAILURE. + * + * In addition to the callback, BluetoothIntent.BONDING_CREATED_ACTION will + * be broadcast if the remote device is successfully bonded. + * + * @param address The remote device Bluetooth address. + * @param callback Your callback, null is ok. + * @return true if your callback was successfully registered, or false if + * there was an error, implying your callback will never be called. + */ + public boolean createBonding(String address, IBluetoothDeviceCallback callback) { + try { + return mService.createBonding(address, callback); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public boolean cancelBondingProcess(String address) { + try { + return mService.cancelBondingProcess(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * List remote devices that are bonded (paired) to the local device. + * + * Bonding (pairing) is the process by which the user enters a pin code for + * the device, which generates a shared link key, allowing for + * authentication and encryption of future connections. In Android we + * require bonding before RFCOMM or SCO connections can be made to a remote + * device. + * + * This function lists which remote devices we have a link key for. It does + * not cause any RF transmission, and does not check if the remote device + * still has it's link key with us. If the other side no longer has its + * link key then the RFCOMM or SCO connection attempt will result in an + * error. + * + * This function does not check if the remote device is in range. + * + * @return bluetooth hardware addresses of remote devices that are + * bonded. Array size is 0 if no devices are bonded. Null on error. + */ + public String[] listBondings() { + try { + return mService.listBondings(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Check if a remote device is bonded (paired) to the local device. + * + * Bonding (pairing) is the process by which the user enters a pin code for + * the device, which generates a shared link key, allowing for + * authentication and encryption of future connections. In Android we + * require bonding before RFCOMM or SCO connections can be made to a remote + * device. + * + * This function checks if we have a link key with the remote device. It + * does not cause any RF transmission, and does not check if the remote + * device still has it's link key with us. If the other side no longer has + * a link key then the RFCOMM or SCO connection attempt will result in an + * error. + * + * This function does not check if the remote device is in range. + * + * @param address Bluetooth hardware address of the remote device to check. + * @return true if bonded, false otherwise and on error. + */ + public boolean hasBonding(String address) { + try { + return mService.hasBonding(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public boolean removeBonding(String address) { + try { + return mService.removeBonding(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public String getRemoteName(String address) { + try { + return mService.getRemoteName(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + public String getRemoteAlias(String address) { + try { + return mService.getRemoteAlias(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public boolean setRemoteAlias(String address, String alias) { + try { + return mService.setRemoteAlias(address, alias); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + public boolean clearRemoteAlias(String address) { + try { + return mService.clearRemoteAlias(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + public String getRemoteVersion(String address) { + try { + return mService.getRemoteVersion(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRemoteRevision(String address) { + try { + return mService.getRemoteRevision(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRemoteManufacturer(String address) { + try { + return mService.getRemoteManufacturer(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRemoteCompany(String address) { + try { + return mService.getRemoteCompany(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRemoteMajorClass(String address) { + try { + return mService.getRemoteMajorClass(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String getRemoteMinorClass(String address) { + try { + return mService.getRemoteMinorClass(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String[] getRemoteServiceClasses(String address) { + try { + return mService.getRemoteServiceClasses(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Returns the RFCOMM channel associated with the 16-byte UUID on + * the remote Bluetooth address. + * + * Performs a SDP ServiceSearchAttributeRequest transaction. The provided + * uuid is verified in the returned record. If there was a problem, or the + * specified uuid does not exist, -1 is returned. + */ + public boolean getRemoteServiceChannel(String address, short uuid16, + IBluetoothDeviceCallback callback) { + try { + return mService.getRemoteServiceChannel(address, uuid16, callback); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + public int getRemoteClass(String address) { + try { + return mService.getRemoteClass(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return DeviceClass.CLASS_UNKNOWN; + } + public byte[] getRemoteFeatures(String address) { + try { + return mService.getRemoteFeatures(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String lastSeen(String address) { + try { + return mService.lastSeen(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + public String lastUsed(String address) { + try { + return mService.lastUsed(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + public boolean setPin(String address, byte[] pin) { + try { + return mService.setPin(address, pin); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + public boolean cancelPin(String address) { + try { + return mService.cancelPin(address); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Check that a pin is valid and convert to byte array. + * + * Bluetooth pin's are 1 to 16 bytes of UTF8 characters. + * @param pin pin as java String + * @return the pin code as a UTF8 byte array, or null if it is an invalid + * Bluetooth pin. + */ + public static byte[] convertPinToBytes(String pin) { + if (pin == null) { + return null; + } + byte[] pinBytes; + try { + pinBytes = pin.getBytes("UTF8"); + } catch (UnsupportedEncodingException uee) { + Log.e(TAG, "UTF8 not supported?!?"); // this should not happen + return null; + } + if (pinBytes.length <= 0 || pinBytes.length > 16) { + return null; + } + return pinBytes; + } + + + /* Sanity check a bluetooth address, such as "00:43:A8:23:10:F0" */ + private static final int ADDRESS_LENGTH = 17; + public static boolean checkBluetoothAddress(String address) { + if (address == null || address.length() != ADDRESS_LENGTH) { + return false; + } + for (int i = 0; i < ADDRESS_LENGTH; i++) { + char c = address.charAt(i); + switch (i % 3) { + case 0: + case 1: + if (Character.digit(c, 16) != -1) { + break; // hex character, OK + } + return false; + case 2: + if (c == ':') { + break; // OK + } + return false; + } + } + return true; + } +} diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java new file mode 100644 index 0000000..90db39b --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.RemoteException; +import android.os.IBinder; +import android.util.Log; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Public API for controlling the Bluetooth Headset Service. + * + * BluetoothHeadset is a proxy object for controlling the Bluetooth Headset + * Service. + * + * Creating a BluetoothHeadset object will create a binding with the + * BluetoothHeadset service. Users of this object should call close() when they + * are finished with the BluetoothHeadset, so that this proxy object can unbind + * from the service. + * + * BlueoothHeadset objects are not guarenteed to be connected to the + * BluetoothHeadsetService at all times. Calls on this object while not + * connected to the service will result in default error return values. Even + * after object construction, there is a short delay (~10ms) before this proxy + * object is actually connected to the Service. + * + * Android only supports one connected Bluetooth Headset at a time. + * + * Note that in this context, Headset includes both Bluetooth Headset's and + * Handsfree devices. + * + * @hide + */ +public class BluetoothHeadset { + + private final static String TAG = "BluetoothHeadset"; + + private final Context mContext; + private IBluetoothHeadset mService; + + /** There was an error trying to obtain the state */ + public static final int STATE_ERROR = -1; + /** No headset currently connected */ + public static final int STATE_DISCONNECTED = 0; + /** Connection attempt in progress */ + public static final int STATE_CONNECTING = 1; + /** A headset is currently connected */ + public static final int STATE_CONNECTED = 2; + + public static final int RESULT_FAILURE = 0; + public static final int RESULT_SUCCESS = 1; + /** Connection cancelled before completetion. */ + public static final int RESULT_CANCELLED = 2; + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + mService = IBluetoothHeadset.Stub.asInterface(service); + Log.i(TAG, "Proxy object is now connected to Bluetooth Headset Service"); + } + public void onServiceDisconnected(ComponentName className) { + mService = null; + } + }; + + /** + * Create a BluetoothHeadset proxy object. + * Remeber to call close() when you are done with this object, so that it + * can unbind from the BluetoothHeadsetService. + */ + public BluetoothHeadset(Context context) { + mContext = context; + if (!context.bindService( + new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) { + Log.e(TAG, "Could not bind to Bluetooth Headset Service"); + } + } + + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Close the connection to the backing service. + * Other public functions of BluetoothHeadset will return default error + * results once close() has been called. Multiple invocations of close() + * are ok. + */ + public synchronized void close() { + if (mConnection != null) { + mContext.unbindService(mConnection); + mConnection = null; + } + } + + /** + * Get the current state of the Bluetooth Headset service. + * @return One of the STATE_ return codes, or STATE_ERROR if this proxy + * object is currently not connected to the Headset service. + */ + public int getState() { + if (mService != null) { + try { + return mService.getState(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } + return BluetoothHeadset.STATE_ERROR; + } + + /** + * Get the Bluetooth address of the current headset. + * @return The Bluetooth address, or null if not in connected or connecting + * state, or if this proxy object is not connected to the Headset + * service. + */ + public String getHeadsetAddress() { + if (mService != null) { + try { + return mService.getHeadsetAddress(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } + return null; + } + + /** + * Request to initiate a connection to a headset. + * This call does not block. Fails if a headset is already connecting + * or connected. + * Will connect to the last connected headset if address is null. + * @param address The Bluetooth Address to connect to, or null to connect + * to the last connected headset. + * @param callback A callback with onCreateBondingResult() defined, or + * null. + * @return False if there was a problem initiating the connection + * procedure, and your callback will not be used. True if + * the connection procedure was initiated, in which case + * your callback is guarenteed to be called. + */ + public boolean connectHeadset(String address, IBluetoothHeadsetCallback callback) { + if (mService != null) { + try { + return mService.connectHeadset(address, callback); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } + return false; + } + + /** + * Returns true if the specified headset is connected (does not include + * connecting). Returns false if not connected, or if this proxy object + * if not currently connected to the headset service. + */ + public boolean isConnected(String address) { + if (mService != null) { + try { + return mService.isConnected(address); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } + return false; + } + + /** + * Disconnects the current headset. Currently this call blocks, it may soon + * be made asynchornous. Returns false if this proxy object is + * not currently connected to the Headset service. + */ + public boolean disconnectHeadset() { + if (mService != null) { + try { + mService.disconnectHeadset(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } + return false; + } +} diff --git a/core/java/android/bluetooth/BluetoothIntent.java b/core/java/android/bluetooth/BluetoothIntent.java new file mode 100644 index 0000000..8e22791 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothIntent.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Manages the local Bluetooth device. Scan for devices, create bondings, + * power up and down the adapter. + * + * @hide + */ +public interface BluetoothIntent { + public static final String MODE = + "android.bluetooth.intent.MODE"; + public static final String ADDRESS = + "android.bluetooth.intent.ADDRESS"; + public static final String NAME = + "android.bluetooth.intent.NAME"; + public static final String ALIAS = + "android.bluetooth.intent.ALIAS"; + public static final String RSSI = + "android.bluetooth.intent.RSSI"; + public static final String CLASS = + "android.bluetooth.intent.CLASS"; + public static final String HEADSET_STATE = + "android.bluetooth.intent.HEADSET_STATE"; + public static final String HEADSET_PREVIOUS_STATE = + "android.bluetooth.intent.HEADSET_PREVIOUS_STATE"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ENABLED_ACTION = + "android.bluetooth.intent.action.ENABLED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DISABLED_ACTION = + "android.bluetooth.intent.action.DISABLED"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String NAME_CHANGED_ACTION = + "android.bluetooth.intent.action.NAME_CHANGED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String MODE_CHANGED_ACTION = + "android.bluetooth.intent.action.MODE_CHANGED"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DISCOVERY_STARTED_ACTION = + "android.bluetooth.intent.action.DISCOVERY_STARTED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DISCOVERY_COMPLETED_ACTION = + "android.bluetooth.intent.action.DISCOVERY_COMPLETED"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String PAIRING_REQUEST_ACTION = + "android.bluetooth.intent.action.PAIRING_REQUEST"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String PAIRING_CANCEL_ACTION = + "android.bluetooth.intent.action.PAIRING_CANCEL"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_FOUND_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_FOUND"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_DISAPPEARED_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_CLASS_UPDATED_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_CONNECTED_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_CONNECTED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECT_REQUESTED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_DEVICE_DISCONNECTED_ACTION = + "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECTED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_NAME_UPDATED_ACTION = + "android.bluetooth.intent.action.REMOTE_NAME_UPDATED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_NAME_FAILED_ACTION = + "android.bluetooth.intent.action.REMOTE_NAME_FAILED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_ALIAS_CHANGED_ACTION = + "android.bluetooth.intent.action.REMOTE_ALIAS_CHANGED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String REMOTE_ALIAS_CLEARED_ACTION = + "android.bluetooth.intent.action.REMOTE_ALIAS_CLEARED"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String BONDING_CREATED_ACTION = + "android.bluetooth.intent.action.BONDING_CREATED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String BONDING_REMOVED_ACTION = + "android.bluetooth.intent.action.BONDING_REMOVED"; + + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String HEADSET_STATE_CHANGED_ACTION = + "android.bluetooth.intent.action.HEADSET_STATE_CHANGED"; +} diff --git a/core/java/android/bluetooth/Database.java b/core/java/android/bluetooth/Database.java new file mode 100644 index 0000000..fef641a --- /dev/null +++ b/core/java/android/bluetooth/Database.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.RfcommSocket; + +import android.util.Log; + +import java.io.*; +import java.util.*; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * A low-level API to the Service Discovery Protocol (SDP) Database. + * + * Allows service records to be added to the local SDP database. Once added, + * these services will be advertised to remote devices when they make SDP + * queries on this device. + * + * Currently this API is a thin wrapper to the bluez SDP Database API. See: + * http://wiki.bluez.org/wiki/Database + * http://wiki.bluez.org/wiki/HOWTO/ManagingServiceRecords + * @hide + */ +public final class Database { + private static Database mInstance; + + private static final String sLogName = "android.bluetooth.Database"; + + /** + * Class load time initialization + */ + static { + classInitNative(); + } + private native static void classInitNative(); + + /** + * Private to enforce singleton property + */ + private Database() { + initializeNativeDataNative(); + } + private native void initializeNativeDataNative(); + + protected void finalize() throws Throwable { + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + private native void cleanupNativeDataNative(); + + /** + * Singelton accessor + * @return The singleton instance of Database + */ + public static synchronized Database getInstance() { + if (mInstance == null) { + mInstance = new Database(); + } + return mInstance; + } + + /** + * Advertise a service with an RfcommSocket. + * + * This adds the service the SDP Database with the following attributes + * set: Service Name, Protocol Descriptor List, Service Class ID List + * TODO: Construct a byte[] record directly, rather than via XML. + * @param socket The rfcomm socket to advertise (by channel). + * @param serviceName A short name for this service + * @param uuid + * Unique identifier for this service, by which clients + * can search for your service + * @return Handle to the new service record + */ + public int advertiseRfcommService(RfcommSocket socket, + String serviceName, + UUID uuid) throws IOException { + String xmlRecord = + "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + + "<record>\n" + + " <attribute id=\"0x0001\">\n" + // ServiceClassIDList + " <sequence>\n" + + " <uuid value=\"" + + uuid.toString() + // UUID for this service + "\"/>\n" + + " </sequence>\n" + + " </attribute>\n" + + " <attribute id=\"0x0004\">\n" + // ProtocolDescriptorList + " <sequence>\n" + + " <sequence>\n" + + " <uuid value=\"0x0100\"/>\n" + // L2CAP + " </sequence>\n" + + " <sequence>\n" + + " <uuid value=\"0x0003\"/>\n" + // RFCOMM + " <uint8 value=\"" + + socket.getPort() + // RFCOMM port + "\" name=\"channel\"/>\n" + + " </sequence>\n" + + " </sequence>\n" + + " </attribute>\n" + + " <attribute id=\"0x0100\">\n" + // ServiceName + " <text value=\"" + serviceName + "\"/>\n" + + " </attribute>\n" + + "</record>\n"; + Log.i(sLogName, xmlRecord); + return addServiceRecordFromXml(xmlRecord); + } + + + /** + * Add a new service record. + * @param record The byte[] record + * @return A handle to the new record + */ + public synchronized int addServiceRecord(byte[] record) throws IOException { + int handle = addServiceRecordNative(record); + Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); + return handle; + } + private native int addServiceRecordNative(byte[] record) + throws IOException; + + /** + * Add a new service record, using XML. + * @param record The record as an XML string + * @return A handle to the new record + */ + public synchronized int addServiceRecordFromXml(String record) throws IOException { + int handle = addServiceRecordFromXmlNative(record); + Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); + return handle; + } + private native int addServiceRecordFromXmlNative(String record) + throws IOException; + + /** + * Update an exisiting service record. + * @param handle Handle to exisiting record + * @param record The updated byte[] record + */ + public synchronized void updateServiceRecord(int handle, byte[] record) { + try { + updateServiceRecordNative(handle, record); + } catch (IOException e) { + Log.e(getClass().toString(), e.getMessage()); + } + } + private native void updateServiceRecordNative(int handle, byte[] record) + throws IOException; + + /** + * Update an exisiting record, using XML. + * @param handle Handle to exisiting record + * @param record The record as an XML string. + */ + public synchronized void updateServiceRecordFromXml(int handle, String record) { + try { + updateServiceRecordFromXmlNative(handle, record); + } catch (IOException e) { + Log.e(getClass().toString(), e.getMessage()); + } + } + private native void updateServiceRecordFromXmlNative(int handle, String record) + throws IOException; + + /** + * Remove a service record. + * It is only possible to remove service records that were added by the + * current connection. + * @param handle Handle to exisiting record to be removed + */ + public synchronized void removeServiceRecord(int handle) { + try { + removeServiceRecordNative(handle); + } catch (IOException e) { + Log.e(getClass().toString(), e.getMessage()); + } + } + private native void removeServiceRecordNative(int handle) throws IOException; +} diff --git a/core/java/android/bluetooth/DeviceClass.java b/core/java/android/bluetooth/DeviceClass.java new file mode 100644 index 0000000..36035ca --- /dev/null +++ b/core/java/android/bluetooth/DeviceClass.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Static helper methods and constants to decode the device class bit vector + * returned by the Bluetooth API. + * + * The Android Bluetooth API returns a 32-bit integer to represent the device + * class. This is actually a bit vector, the format defined at + * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm + * (login required). This class provides static helper methods and constants to + * determine what Service Class(es), Major Class, and Minor Class are encoded + * in a 32-bit device class. + * + * Each of the helper methods takes the 32-bit integer device class as an + * argument. + * + * @hide + */ +public class DeviceClass { + + // Baseband class information + // See http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm + + public static final int SERVICE_CLASS_BITMASK = 0xFFE000; + public static final int SERVICE_CLASS_LIMITED_DISCOVERABILITY = 0x002000; + public static final int SERVICE_CLASS_POSITIONING = 0x010000; + public static final int SERVICE_CLASS_NETWORKING = 0x020000; + public static final int SERVICE_CLASS_RENDER = 0x040000; + public static final int SERVICE_CLASS_CAPTURE = 0x080000; + public static final int SERVICE_CLASS_OBJECT_TRANSFER = 0x100000; + public static final int SERVICE_CLASS_AUDIO = 0x200000; + public static final int SERVICE_CLASS_TELEPHONY = 0x400000; + public static final int SERVICE_CLASS_INFORMATION = 0x800000; + + public static final int MAJOR_CLASS_BITMASK = 0x001F00; + public static final int MAJOR_CLASS_MISC = 0x000000; + public static final int MAJOR_CLASS_COMPUTER = 0x000100; + public static final int MAJOR_CLASS_PHONE = 0x000200; + public static final int MAJOR_CLASS_NETWORKING = 0x000300; + public static final int MAJOR_CLASS_AUDIO_VIDEO = 0x000400; + public static final int MAJOR_CLASS_PERIPHERAL = 0x000500; + public static final int MAJOR_CLASS_IMAGING = 0x000600; + public static final int MAJOR_CLASS_WEARABLE = 0x000700; + public static final int MAJOR_CLASS_TOY = 0x000800; + public static final int MAJOR_CLASS_MEDICAL = 0x000900; + public static final int MAJOR_CLASS_UNCATEGORIZED = 0x001F00; + + // Minor classes for the AUDIO_VIDEO major class + public static final int MINOR_CLASS_AUDIO_VIDEO_BITMASK = 0x0000FC; + public static final int MINOR_CLASS_AUDIO_VIDEO_UNCATEGORIZED = 0x000000; + public static final int MINOR_CLASS_AUDIO_VIDEO_HEADSET = 0x000004; + public static final int MINOR_CLASS_AUDIO_VIDEO_HANDSFREE = 0x000008; + public static final int MINOR_CLASS_AUDIO_VIDEO_MICROPHONE = 0x000010; + public static final int MINOR_CLASS_AUDIO_VIDEO_LOUDSPEAKER = 0x000014; + public static final int MINOR_CLASS_AUDIO_VIDEO_HEADPHONES = 0x000018; + public static final int MINOR_CLASS_AUDIO_VIDEO_PORTABLE_AUDIO = 0x00001C; + public static final int MINOR_CLASS_AUDIO_VIDEO_CAR_AUDIO = 0x000020; + public static final int MINOR_CLASS_AUDIO_VIDEO_SET_TOP_BOX = 0x000024; + public static final int MINOR_CLASS_AUDIO_VIDEO_HIFI_AUDIO = 0x000028; + public static final int MINOR_CLASS_AUDIO_VIDEO_VCR = 0x00002C; + public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CAMERA = 0x000030; + public static final int MINOR_CLASS_AUDIO_VIDEO_CAMCORDER = 0x000034; + public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_MONITOR = 0x000038; + public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x00003C; + public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CONFERENCING = 0x000040; + public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x000048; + + // Indicates the Bluetooth API could not retrieve the class + public static final int CLASS_UNKNOWN = 0xFF000000; + + /** Returns true if the given device class supports the given Service Class. + * A bluetooth device can claim to support zero or more service classes. + * @param deviceClass The bluetooth device class. + * @param serviceClassType The service class constant to test for. For + * example, DeviceClass.SERVICE_CLASS_AUDIO. This + * must be one of the SERVICE_CLASS_xxx constants, + * results of this function are undefined + * otherwise. + * @return If the deviceClass claims to support the serviceClassType. + */ + public static boolean hasServiceClass(int deviceClass, int serviceClassType) { + if (deviceClass == CLASS_UNKNOWN) { + return false; + } + return ((deviceClass & SERVICE_CLASS_BITMASK & serviceClassType) != 0); + } + + /** Returns the Major Class of a bluetooth device class. + * Values returned from this function can be compared with the constants + * MAJOR_CLASS_xxx. A bluetooth device can only be associated + * with one major class. + */ + public static int getMajorClass(int deviceClass) { + if (deviceClass == CLASS_UNKNOWN) { + return CLASS_UNKNOWN; + } + return (deviceClass & MAJOR_CLASS_BITMASK); + } + + /** Returns the Minor Class of a bluetooth device class. + * Values returned from this function can be compared with the constants + * MINOR_CLASS_xxx_yyy, where xxx is the Major Class. A bluetooth + * device can only be associated with one minor class within its major + * class. + */ + public static int getMinorClass(int deviceClass) { + if (deviceClass == CLASS_UNKNOWN) { + return CLASS_UNKNOWN; + } + return (deviceClass & MINOR_CLASS_AUDIO_VIDEO_BITMASK); + } +} diff --git a/core/java/android/bluetooth/HeadsetBase.java b/core/java/android/bluetooth/HeadsetBase.java new file mode 100644 index 0000000..bce3388 --- /dev/null +++ b/core/java/android/bluetooth/HeadsetBase.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.os.Handler; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +import java.io.IOException; +import java.lang.Thread; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * The base RFCOMM (service) connection for a headset or handsfree device. + * + * In the future this class will be removed. + * + * @hide + */ +public class HeadsetBase { + private static final String TAG = "Bluetooth HeadsetBase"; + private static final boolean DBG = false; + + public static final int RFCOMM_DISCONNECTED = 1; + + public static final int DIRECTION_INCOMING = 1; + public static final int DIRECTION_OUTGOING = 2; + + private final BluetoothDevice mBluetooth; + private final String mAddress; + private final int mRfcommChannel; + private int mNativeData; + private Thread mEventThread; + private volatile boolean mEventThreadInterrupted; + private Handler mEventThreadHandler; + private int mTimeoutRemainingMs; + private final int mDirection; + private final long mConnectTimestamp; + + protected AtParser mAtParser; + + private WakeLock mWakeLock; // held while processing an AT command + + private native static void classInitNative(); + static { + classInitNative(); + } + + protected void finalize() throws Throwable { + try { + cleanupNativeDataNative(); + releaseWakeLock(); + } finally { + super.finalize(); + } + } + + private native void cleanupNativeDataNative(); + + public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address, + int rfcommChannel) { + mDirection = DIRECTION_OUTGOING; + mConnectTimestamp = System.currentTimeMillis(); + mBluetooth = bluetooth; + mAddress = address; + mRfcommChannel = rfcommChannel; + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase"); + mWakeLock.setReferenceCounted(false); + initializeAtParser(); + // Must be called after this.mAddress is set. + initializeNativeDataNative(-1); + } + + /* Create from an already exisiting rfcomm connection */ + public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address, int socketFd, + int rfcommChannel, Handler handler) { + mDirection = DIRECTION_INCOMING; + mConnectTimestamp = System.currentTimeMillis(); + mBluetooth = bluetooth; + mAddress = address; + mRfcommChannel = rfcommChannel; + mEventThreadHandler = handler; + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase"); + mWakeLock.setReferenceCounted(false); + initializeAtParser(); + // Must be called after this.mAddress is set. + initializeNativeDataNative(socketFd); + } + + private native void initializeNativeDataNative(int socketFd); + + /* Process an incoming AT command line + */ + protected synchronized void handleInput(String input) { + acquireWakeLock(); + long timestamp; + + if (DBG) timestamp = System.currentTimeMillis(); + AtCommandResult result = mAtParser.process(input); + if (DBG) Log.d(TAG, "Processing " + input + " took " + + (System.currentTimeMillis() - timestamp) + " ms"); + + if (result.getResultCode() == AtCommandResult.ERROR) { + Log.i(TAG, "Error pocessing <" + input + ">"); + } + + sendURC(result.toString()); + + releaseWakeLock(); + } + + /** + * Register AT commands that are common to all Headset / Handsets. This + * function is called by the HeadsetBase constructor. + */ + protected void initializeAtParser() { + mAtParser = new AtParser(); + + // Microphone Gain + mAtParser.register("+VGM", new AtCommandHandler() { + @Override + public AtCommandResult handleSetCommand(Object[] args) { + // AT+VGM=<gain> in range [0,15] + // Headset/Handsfree is reporting its current gain setting + //TODO: sync to android UI + //TODO: Send unsolicited +VGM when volume changed on AG + return new AtCommandResult(AtCommandResult.OK); + } + }); + + // Speaker Gain + mAtParser.register("+VGS", new AtCommandHandler() { + @Override + public AtCommandResult handleSetCommand(Object[] args) { + // AT+VGS=<gain> in range [0,15] + // Headset/Handsfree is reporting its current gain to Android + //TODO: sync to AG UI + //TODO: Send unsolicited +VGS when volume changed on AG + return new AtCommandResult(AtCommandResult.OK); + } + }); + } + + public AtParser getAtParser() { + return mAtParser; + } + + public void startEventThread() { + mEventThread = + new Thread("HeadsetBase Event Thread") { + public void run() { + int last_read_error; + while (!mEventThreadInterrupted) { + String input = readNative(500); + if (input != null) { + handleInput(input); + } + else { + last_read_error = getLastReadStatusNative(); + if (last_read_error != 0) { + Log.i(TAG, "headset read error " + last_read_error); + if (mEventThreadHandler != null) { + mEventThreadHandler.obtainMessage(RFCOMM_DISCONNECTED) + .sendToTarget(); + } + disconnectNative(); + break; + } + } + } + } + }; + mEventThreadInterrupted = false; + mEventThread.start(); + } + + + + private native String readNative(int timeout_ms); + private native int getLastReadStatusNative(); + + private void stopEventThread() { + mEventThreadInterrupted = true; + mEventThread.interrupt(); + try { + mEventThread.join(); + } catch (java.lang.InterruptedException e) { + // FIXME: handle this, + } + mEventThread = null; + } + + public boolean connect(Handler handler) { + if (mEventThread == null) { + if (!connectNative()) return false; + mEventThreadHandler = handler; + } + return true; + } + private native boolean connectNative(); + + /* + * Returns true when either the asynchronous connect is in progress, or + * the connect is complete. Call waitForAsyncConnect() to find out whether + * the connect is actually complete, or disconnect() to cancel. + */ + + public boolean connectAsync() { + return connectAsyncNative(); + } + private native boolean connectAsyncNative(); + + public int getRemainingAsyncConnectWaitingTimeMs() { + return mTimeoutRemainingMs; + } + + /* + * Returns 1 when an async connect is complete, 0 on timeout, and -1 on + * error. On error, handler will be called, and you need to re-initiate + * the async connect. + */ + public int waitForAsyncConnect(int timeout_ms, Handler handler) { + int res = waitForAsyncConnectNative(timeout_ms); + if (res > 0) { + mEventThreadHandler = handler; + } + return res; + } + private native int waitForAsyncConnectNative(int timeout_ms); + + public void disconnect() { + if (mEventThread != null) { + stopEventThread(); + } + disconnectNative(); + } + private native void disconnectNative(); + + + /* + * Note that if a remote side disconnects, this method will still return + * true until disconnect() is called. You know when a remote side + * disconnects because you will receive the intent + * IBluetoothService.REMOTE_DEVICE_DISCONNECTED_ACTION. If, when you get + * this intent, method isConnected() returns true, you know that the + * disconnect was initiated by the remote device. + */ + + public boolean isConnected() { + return mEventThread != null; + } + + public String getAddress() { + return mAddress; + } + + public String getName() { + return mBluetooth.getRemoteName(mAddress); + } + + public int getDirection() { + return mDirection; + } + + public long getConnectTimestamp() { + return mConnectTimestamp; + } + + public synchronized boolean sendURC(String urc) { + if (urc.length() > 0) { + boolean ret = sendURCNative(urc); + return ret; + } + return true; + } + private native boolean sendURCNative(String urc); + + private void acquireWakeLock() { + if (!mWakeLock.isHeld()) { + mWakeLock.acquire(); + } + } + + private void releaseWakeLock() { + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + + private void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/IBluetoothDevice.aidl b/core/java/android/bluetooth/IBluetoothDevice.aidl new file mode 100644 index 0000000..e7cc8ed --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothDevice.aidl @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.IBluetoothDeviceCallback; + +/** + * System private API for talking with the Bluetooth service. + * + * {@hide} + */ +interface IBluetoothDevice +{ + boolean isEnabled(); + boolean enable(in IBluetoothDeviceCallback callback); // async + boolean disable(); + + String getAddress(); + String getName(); + boolean setName(in String name); + String getMajorClass(); + String getMinorClass(); + String getVersion(); + String getRevision(); + String getManufacturer(); + String getCompany(); + + int getMode(); + boolean setMode(int mode); + + int getDiscoverableTimeout(); + boolean setDiscoverableTimeout(int timeout); + + boolean startDiscovery(boolean resolveNames); + boolean cancelDiscovery(); + boolean isDiscovering(); + boolean startPeriodicDiscovery(); + boolean stopPeriodicDiscovery(); + boolean isPeriodicDiscovery(); + String[] listRemoteDevices(); + + String[] listAclConnections(); + boolean isAclConnected(in String address); + boolean disconnectRemoteDeviceAcl(in String address); + + boolean createBonding(in String address, in IBluetoothDeviceCallback callback); + boolean cancelBondingProcess(in String address); + String[] listBondings(); + boolean hasBonding(in String address); + boolean removeBonding(in String address); + + String getRemoteName(in String address); + String getRemoteAlias(in String address); + boolean setRemoteAlias(in String address, in String alias); + boolean clearRemoteAlias(in String address); + String getRemoteVersion(in String address); + String getRemoteRevision(in String address); + int getRemoteClass(in String address); + String getRemoteManufacturer(in String address); + String getRemoteCompany(in String address); + String getRemoteMajorClass(in String address); + String getRemoteMinorClass(in String address); + String[] getRemoteServiceClasses(in String address); + boolean getRemoteServiceChannel(in String address, int uuid16, in IBluetoothDeviceCallback callback); + byte[] getRemoteFeatures(in String adddress); + String lastSeen(in String address); + String lastUsed(in String address); + + boolean setPin(in String address, in byte[] pin); + boolean cancelPin(in String address); +} diff --git a/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl b/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl new file mode 100644 index 0000000..86f44dd --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * {@hide} + */ +oneway interface IBluetoothDeviceCallback +{ + void onCreateBondingResult(in String address, int result); + void onGetRemoteServiceChannelResult(in String address, int channel); + + void onEnableResult(int result); +} diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl new file mode 100644 index 0000000..7b6030b --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.IBluetoothHeadsetCallback; + +/** + * System private API for Bluetooth Headset service + * + * {@hide} + */ +interface IBluetoothHeadset { + int getState(); + + String getHeadsetAddress(); + + // Request that the given headset be connected + // Assumes the given headset is already bonded + // Will disconnect any currently connected headset + // returns false if cannot start a connection (for example, there is + // already a pending connect). callback will always be called iff this + // returns true + boolean connectHeadset(in String address, in IBluetoothHeadsetCallback callback); + + boolean isConnected(in String address); + + void disconnectHeadset(); +} diff --git a/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl b/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl new file mode 100644 index 0000000..03e884b --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * {@hide} + */ +oneway interface IBluetoothHeadsetCallback +{ + void onConnectHeadsetResult(in String address, int resultCode); +} diff --git a/core/java/android/bluetooth/RfcommSocket.java b/core/java/android/bluetooth/RfcommSocket.java new file mode 100644 index 0000000..a33263f --- /dev/null +++ b/core/java/android/bluetooth/RfcommSocket.java @@ -0,0 +1,674 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import java.io.IOException; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.FileDescriptor; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * This class implements an API to the Bluetooth RFCOMM layer. An RFCOMM socket + * is similar to a normal socket in that it takes an address and a port number. + * The difference is of course that the address is a Bluetooth-device address, + * and the port number is an RFCOMM channel. The API allows for the + * establishment of listening sockets via methods + * {@link #bind(String, int) bind}, {@link #listen(int) listen}, and + * {@link #accept(RfcommSocket, int) accept}, as well as for the making of + * outgoing connections with {@link #connect(String, int) connect}, + * {@link #connectAsync(String, int) connectAsync}, and + * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. + * + * After constructing a socket, you need to {@link #create() create} it and then + * {@link #destroy() destroy} it when you are done using it. Both + * {@link #create() create} and {@link #accept(RfcommSocket, int) accept} return + * a {@link java.io.FileDescriptor FileDescriptor} for the actual data. + * Alternatively, you may call {@link #getInputStream() getInputStream} and + * {@link #getOutputStream() getOutputStream} to retrieve the respective streams + * without going through the FileDescriptor. + * + * @hide + */ +public class RfcommSocket { + + /** + * Used by the native implementation of the class. + */ + private int mNativeData; + + /** + * Used by the native implementation of the class. + */ + private int mPort; + + /** + * Used by the native implementation of the class. + */ + private String mAddress; + + /** + * We save the return value of {@link #create() create} and + * {@link #accept(RfcommSocket,int) accept} in this variable, and use it to + * retrieve the I/O streams. + */ + private FileDescriptor mFd; + + /** + * After a call to {@link #waitForAsyncConnect(int) waitForAsyncConnect}, + * if the return value is zero, then, the the remaining time left to wait is + * written into this variable (by the native implementation). It is possible + * that {@link #waitForAsyncConnect(int) waitForAsyncConnect} returns before + * the user-specified timeout expires, which is why we save the remaining + * time in this member variable for the user to retrieve by calling method + * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs}. + */ + private int mTimeoutRemainingMs; + + /** + * Set to true when an asynchronous (nonblocking) connect is in progress. + * {@see #connectAsync(String,int)}. + */ + private boolean mIsConnecting; + + /** + * Set to true after a successful call to {@link #bind(String,int) bind} and + * used for error checking in {@link #listen(int) listen}. Reset to false + * on {@link #destroy() destroy}. + */ + private boolean mIsBound = false; + + /** + * Set to true after a successful call to {@link #listen(int) listen} and + * used for error checking in {@link #accept(RfcommSocket,int) accept}. + * Reset to false on {@link #destroy() destroy}. + */ + private boolean mIsListening = false; + + /** + * Used to store the remaining time after an accept with a non-negative + * timeout returns unsuccessfully. It is possible that a blocking + * {@link #accept(int) accept} may wait for less than the time specified by + * the user, which is why we store the remainder in this member variable for + * it to be retrieved with method + * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs}. + */ + private int mAcceptTimeoutRemainingMs; + + /** + * Maintained by {@link #getInputStream() getInputStream}. + */ + protected FileInputStream mInputStream; + + /** + * Maintained by {@link #getOutputStream() getOutputStream}. + */ + protected FileOutputStream mOutputStream; + + private native void initializeNativeDataNative(); + + /** + * Constructor. + */ + public RfcommSocket() { + initializeNativeDataNative(); + } + + private native void cleanupNativeDataNative(); + + /** + * Called by the GC to clean up the native data that we set up when we + * construct the object. + */ + protected void finalize() throws Throwable { + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + + private native static void classInitNative(); + + static { + classInitNative(); + } + + /** + * Creates a socket. You need to call this method before performing any + * other operation on a socket. + * + * @return FileDescriptor for the data stream. + * @throws IOException + * @see #destroy() + */ + public FileDescriptor create() throws IOException { + if (mFd == null) { + mFd = createNative(); + } + if (mFd == null) { + throw new IOException("socket not created"); + } + return mFd; + } + + private native FileDescriptor createNative(); + + /** + * Destroys a socket created by {@link #create() create}. Call this + * function when you no longer use the socket in order to release the + * underlying OS resources. + * + * @see #create() + */ + public void destroy() { + synchronized (this) { + destroyNative(); + mFd = null; + mIsBound = false; + mIsListening = false; + } + } + + private native void destroyNative(); + + /** + * Returns the {@link java.io.FileDescriptor FileDescriptor} of the socket. + * + * @return the FileDescriptor + * @throws IOException + * when the socket has not been {@link #create() created}. + */ + public FileDescriptor getFileDescriptor() throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + return mFd; + } + + /** + * Retrieves the input stream from the socket. Alternatively, you can do + * that from the FileDescriptor returned by {@link #create() create} or + * {@link #accept(RfcommSocket, int) accept}. + * + * @return InputStream + * @throws IOException + * if you have not called {@link #create() create} on the + * socket. + */ + public InputStream getInputStream() throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + + synchronized (this) { + if (mInputStream == null) { + mInputStream = new FileInputStream(mFd); + } + + return mInputStream; + } + } + + /** + * Retrieves the output stream from the socket. Alternatively, you can do + * that from the FileDescriptor returned by {@link #create() create} or + * {@link #accept(RfcommSocket, int) accept}. + * + * @return OutputStream + * @throws IOException + * if you have not called {@link #create() create} on the + * socket. + */ + public OutputStream getOutputStream() throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + + synchronized (this) { + if (mOutputStream == null) { + mOutputStream = new FileOutputStream(mFd); + } + + return mOutputStream; + } + } + + /** + * Starts a blocking connect to a remote RFCOMM socket. It takes the address + * of a device and the RFCOMM channel (port) to which to connect. + * + * @param address + * is the Bluetooth address of the remote device. + * @param port + * is the RFCOMM channel + * @return true on success, false on failure + * @throws IOException + * if {@link #create() create} has not been called. + * @see #connectAsync(String, int) + */ + public boolean connect(String address, int port) throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + return connectNative(address, port); + } + } + + private native boolean connectNative(String address, int port); + + /** + * Starts an asynchronous (nonblocking) connect to a remote RFCOMM socket. + * It takes the address of the device to connect to, as well as the RFCOMM + * channel (port). On successful return (return value is true), you need to + * call method {@link #waitForAsyncConnect(int) waitForAsyncConnect} to + * block for up to a specified number of milliseconds while waiting for the + * asyncronous connect to complete. + * + * @param address + * of remote device + * @param port + * the RFCOMM channel + * @return true when the asynchronous connect has successfully started, + * false if there was an error. + * @throws IOException + * is you have not called {@link #create() create} + * @see #waitForAsyncConnect(int) + * @see #getRemainingAsyncConnectWaitingTimeMs() + * @see #connect(String, int) + */ + public boolean connectAsync(String address, int port) throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + mIsConnecting = connectAsyncNative(address, port); + return mIsConnecting; + } + } + + private native boolean connectAsyncNative(String address, int port); + + /** + * Interrupts an asynchronous connect in progress. This method does nothing + * when there is no asynchronous connect in progress. + * + * @throws IOException + * if you have not called {@link #create() create}. + * @see #connectAsync(String, int) + */ + public void interruptAsyncConnect() throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + if (mIsConnecting) { + mIsConnecting = !interruptAsyncConnectNative(); + } + } + } + + private native boolean interruptAsyncConnectNative(); + + /** + * Tells you whether there is an asynchronous connect in progress. This + * method returns an undefined value when there is a synchronous connect in + * progress. + * + * @return true if there is an asyc connect in progress, false otherwise + * @see #connectAsync(String, int) + */ + public boolean isConnecting() { + return mIsConnecting; + } + + /** + * Blocks for a specified amount of milliseconds while waiting for an + * asynchronous connect to complete. Returns an integer value to indicate + * one of the following: the connect succeeded, the connect is still in + * progress, or the connect failed. It is possible for this method to block + * for less than the time specified by the user, and still return zero + * (i.e., async connect is still in progress.) For this reason, if the + * return value is zero, you need to call method + * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs} + * to retrieve the remaining time. + * + * @param timeoutMs + * the time to block while waiting for the async connect to + * complete. + * @return a positive value if the connect succeeds; zero, if the connect is + * still in progress, and a negative value if the connect failed. + * + * @throws IOException + * @see #getRemainingAsyncConnectWaitingTimeMs() + * @see #connectAsync(String, int) + */ + public int waitForAsyncConnect(int timeoutMs) throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + int ret = waitForAsyncConnectNative(timeoutMs); + if (ret != 0) { + mIsConnecting = false; + } + return ret; + } + } + + private native int waitForAsyncConnectNative(int timeoutMs); + + /** + * Returns the number of milliseconds left to wait after the last call to + * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. + * + * It is possible that waitForAsyncConnect() waits for less than the time + * specified by the user, and still returns zero (i.e., async connect is + * still in progress.) For this reason, if the return value is zero, you + * need to call this method to retrieve the remaining time before you call + * waitForAsyncConnect again. + * + * @return the remaining timeout in milliseconds. + * @see #waitForAsyncConnect(int) + * @see #connectAsync(String, int) + */ + public int getRemainingAsyncConnectWaitingTimeMs() { + return mTimeoutRemainingMs; + } + + /** + * Shuts down both directions on a socket. + * + * @return true on success, false on failure; if the return value is false, + * the socket might be left in a patially shut-down state (i.e. one + * direction is shut down, but the other is still open.) In this + * case, you should {@link #destroy() destroy} and then + * {@link #create() create} the socket again. + * @throws IOException + * is you have not caled {@link #create() create}. + * @see #shutdownInput() + * @see #shutdownOutput() + */ + public boolean shutdown() throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + if (shutdownNative(true)) { + return shutdownNative(false); + } + + return false; + } + } + + /** + * Shuts down the input stream of the socket, but leaves the output stream + * in its current state. + * + * @return true on success, false on failure + * @throws IOException + * is you have not called {@link #create() create} + * @see #shutdown() + * @see #shutdownOutput() + */ + public boolean shutdownInput() throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + return shutdownNative(true); + } + } + + /** + * Shut down the output stream of the socket, but leaves the input stream in + * its current state. + * + * @return true on success, false on failure + * @throws IOException + * is you have not called {@link #create() create} + * @see #shutdown() + * @see #shutdownInput() + */ + public boolean shutdownOutput() throws IOException { + synchronized (this) { + if (mFd == null) { + throw new IOException("socket not created"); + } + return shutdownNative(false); + } + } + + private native boolean shutdownNative(boolean shutdownInput); + + /** + * Tells you whether a socket is connected to another socket. This could be + * for input or output or both. + * + * @return true if connected, false otherwise. + * @see #isInputConnected() + * @see #isOutputConnected() + */ + public boolean isConnected() { + return isConnectedNative() > 0; + } + + /** + * Determines whether input is connected (i.e., whether you can receive data + * on this socket.) + * + * @return true if input is connected, false otherwise. + * @see #isConnected() + * @see #isOutputConnected() + */ + public boolean isInputConnected() { + return (isConnectedNative() & 1) != 0; + } + + /** + * Determines whether output is connected (i.e., whether you can send data + * on this socket.) + * + * @return true if output is connected, false otherwise. + * @see #isConnected() + * @see #isInputConnected() + */ + public boolean isOutputConnected() { + return (isConnectedNative() & 2) != 0; + } + + private native int isConnectedNative(); + + /** + * Binds a listening socket to the local device, or a non-listening socket + * to a remote device. The port is automatically selected as the first + * available port in the range 12 to 30. + * + * NOTE: Currently we ignore the device parameter and always bind the socket + * to the local device, assuming that it is a listening socket. + * + * TODO: Use bind(0) in native code to have the kernel select an unused + * port. + * + * @param device + * Bluetooth address of device to bind to (currently ignored). + * @return true on success, false on failure + * @throws IOException + * if you have not called {@link #create() create} + * @see #listen(int) + * @see #accept(RfcommSocket,int) + */ + public boolean bind(String device) throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + for (int port = 12; port <= 30; port++) { + if (bindNative(device, port)) { + mIsBound = true; + return true; + } + } + mIsBound = false; + return false; + } + + /** + * Binds a listening socket to the local device, or a non-listening socket + * to a remote device. + * + * NOTE: Currently we ignore the device parameter and always bind the socket + * to the local device, assuming that it is a listening socket. + * + * @param device + * Bluetooth address of device to bind to (currently ignored). + * @param port + * RFCOMM channel to bind socket to. + * @return true on success, false on failure + * @throws IOException + * if you have not called {@link #create() create} + * @see #listen(int) + * @see #accept(RfcommSocket,int) + */ + public boolean bind(String device, int port) throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + mIsBound = bindNative(device, port); + return mIsBound; + } + + private native boolean bindNative(String device, int port); + + /** + * Starts listening for incoming connections on this socket, after it has + * been bound to an address and RFCOMM channel with + * {@link #bind(String,int) bind}. + * + * @param backlog + * the number of pending incoming connections to queue for + * {@link #accept(RfcommSocket, int) accept}. + * @return true on success, false on failure + * @throws IOException + * if you have not called {@link #create() create} or if the + * socket has not been bound to a device and RFCOMM channel. + */ + public boolean listen(int backlog) throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + if (!mIsBound) { + throw new IOException("socket not bound"); + } + mIsListening = listenNative(backlog); + return mIsListening; + } + + private native boolean listenNative(int backlog); + + /** + * Accepts incoming-connection requests for a listening socket bound to an + * RFCOMM channel. The user may provide a time to wait for an incoming + * connection. + * + * Note that this method may return null (i.e., no incoming connection) + * before the user-specified timeout expires. For this reason, on a null + * return value, you need to call + * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs} + * in order to see how much time is left to wait, before you call this + * method again. + * + * @param newSock + * is set to the new socket that is created as a result of a + * successful accept. + * @param timeoutMs + * time (in milliseconds) to block while waiting to an + * incoming-connection request. A negative value is an infinite + * wait. + * @return FileDescriptor of newSock on success, null on failure. Failure + * occurs if the timeout expires without a successful connect. + * @throws IOException + * if the socket has not been {@link #create() create}ed, is + * not bound, or is not a listening socket. + * @see #bind(String, int) + * @see #listen(int) + * @see #getRemainingAcceptWaitingTimeMs() + */ + public FileDescriptor accept(RfcommSocket newSock, int timeoutMs) + throws IOException { + synchronized (newSock) { + if (mFd == null) { + throw new IOException("socket not created"); + } + if (mIsListening == false) { + throw new IOException("not listening on socket"); + } + newSock.mFd = acceptNative(newSock, timeoutMs); + return newSock.mFd; + } + } + + /** + * Returns the number of milliseconds left to wait after the last call to + * {@link #accept(RfcommSocket, int) accept}. + * + * Since accept() may return null (i.e., no incoming connection) before the + * user-specified timeout expires, you need to call this method in order to + * see how much time is left to wait, and wait for that amount of time + * before you call accept again. + * + * @return the remaining time, in milliseconds. + */ + public int getRemainingAcceptWaitingTimeMs() { + return mAcceptTimeoutRemainingMs; + } + + private native FileDescriptor acceptNative(RfcommSocket newSock, + int timeoutMs); + + /** + * Get the port (rfcomm channel) associated with this socket. + * + * This is only valid if the port has been set via a successful call to + * {@link #bind(String, int)}, {@link #connect(String, int)} + * or {@link #connectAsync(String, int)}. This can be checked + * with {@link #isListening()} and {@link #isConnected()}. + * @return Port (rfcomm channel) + */ + public int getPort() throws IOException { + if (mFd == null) { + throw new IOException("socket not created"); + } + if (!mIsListening && !isConnected()) { + throw new IOException("not listening or connected on socket"); + } + return mPort; + } + + /** + * Return true if this socket is listening ({@link #listen(int)} + * has been called successfully). + */ + public boolean isListening() { + return mIsListening; + } +} diff --git a/core/java/android/bluetooth/ScoSocket.java b/core/java/android/bluetooth/ScoSocket.java new file mode 100644 index 0000000..75b3329 --- /dev/null +++ b/core/java/android/bluetooth/ScoSocket.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +import java.io.IOException; +import java.lang.Thread; + + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Simple SCO Socket. + * Currently in Android, there is no support for sending data over a SCO + * socket - this is managed by the hardware link to the Bluetooth Chip. This + * class is instead intended for management of the SCO socket lifetime, + * and is tailored for use with the headset / handsfree profiles. + * @hide + */ +public class ScoSocket { + private static final String TAG = "ScoSocket"; + private static final boolean DBG = true; + private static final boolean VDBG = false; // even more logging + + public static final int STATE_READY = 1; // Ready for use. No threads or sockets + public static final int STATE_ACCEPT = 2; // accept() thread running + public static final int STATE_CONNECTING = 3; // connect() thread running + public static final int STATE_CONNECTED = 4; // connected, waiting for close() + public static final int STATE_CLOSED = 5; // was connected, now closed. + + private int mState; + private int mNativeData; + private Handler mHandler; + private int mAcceptedCode; + private int mConnectedCode; + private int mClosedCode; + + private WakeLock mWakeLock; // held while STATE_CONNECTING or STATE_CONNECTED + + static { + classInitNative(); + } + private native static void classInitNative(); + + public ScoSocket(PowerManager pm, Handler handler, int acceptedCode, int connectedCode, + int closedCode) { + initNative(); + mState = STATE_READY; + mHandler = handler; + mAcceptedCode = acceptedCode; + mConnectedCode = connectedCode; + mClosedCode = closedCode; + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ScoSocket"); + mWakeLock.setReferenceCounted(false); + if (VDBG) log(this + " SCO OBJECT CTOR"); + } + private native void initNative(); + + protected void finalize() throws Throwable { + try { + if (VDBG) log(this + " SCO OBJECT DTOR"); + destroyNative(); + releaseWakeLock(); + } finally { + super.finalize(); + } + } + private native void destroyNative(); + + /** Connect this SCO socket to the given BT address. + * Does not block. + */ + public synchronized boolean connect(String address) { + if (VDBG) log("connect() " + this); + if (mState != STATE_READY) { + if (DBG) log("connect(): Bad state"); + return false; + } + acquireWakeLock(); + if (connectNative(address)) { + mState = STATE_CONNECTING; + return true; + } else { + mState = STATE_CLOSED; + releaseWakeLock(); + return false; + } + } + private native boolean connectNative(String address); + + /** Accept incoming SCO connections. + * Does not block. + */ + public synchronized boolean accept() { + if (VDBG) log("accept() " + this); + if (mState != STATE_READY) { + if (DBG) log("Bad state"); + return false; + } + if (acceptNative()) { + mState = STATE_ACCEPT; + return true; + } else { + mState = STATE_CLOSED; + return false; + } + } + private native boolean acceptNative(); + + public synchronized void close() { + if (DBG) log(this + " SCO OBJECT close() mState = " + mState); + mState = STATE_CLOSED; + closeNative(); + releaseWakeLock(); + } + private native void closeNative(); + + public synchronized int getState() { + return mState; + } + + private synchronized void onConnected(int result) { + if (VDBG) log(this + " onConnected() mState = " + mState + " " + this); + if (mState != STATE_CONNECTING) { + if (DBG) log("Strange state, closing " + mState + " " + this); + return; + } + if (result >= 0) { + mState = STATE_CONNECTED; + } else { + mState = STATE_CLOSED; + } + mHandler.obtainMessage(mConnectedCode, mState, -1, this).sendToTarget(); + if (result < 0) { + releaseWakeLock(); + } + } + + private synchronized void onAccepted(int result) { + if (VDBG) log("onAccepted() " + this); + if (mState != STATE_ACCEPT) { + if (DBG) log("Strange state" + this); + return; + } + if (result >= 0) { + acquireWakeLock(); + mState = STATE_CONNECTED; + } else { + mState = STATE_CLOSED; + } + mHandler.obtainMessage(mAcceptedCode, mState, -1, this).sendToTarget(); + } + + private synchronized void onClosed() { + if (DBG) log("onClosed() " + this); + if (mState != STATE_CLOSED) { + mState = STATE_CLOSED; + mHandler.obtainMessage(mClosedCode, mState, -1, this).sendToTarget(); + releaseWakeLock(); + } + } + + private void acquireWakeLock() { + if (!mWakeLock.isHeld()) { + mWakeLock.acquire(); + if (VDBG) log("mWakeLock.acquire()" + this); + } + } + + private void releaseWakeLock() { + if (mWakeLock.isHeld()) { + if (VDBG) log("mWakeLock.release()" + this); + mWakeLock.release(); + } + } + + private void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/package.html b/core/java/android/bluetooth/package.html new file mode 100644 index 0000000..ccd8fec --- /dev/null +++ b/core/java/android/bluetooth/package.html @@ -0,0 +1,14 @@ +<HTML> +<BODY> +Provides classes that manage Bluetooth functionality on the device. +<p> +The Bluetooth APIs allow applications can connect and disconnect headsets, or scan +for other kinds of Bluetooth devices and pair them. Further control includes the +ability to write and modify the local Service Discovery Protocol (SDP) database, +query the SDP database of other Bluetooth devices, establish RFCOMM +channels/sockets on Android, and connect to specified sockets on other devices. +</p> +<p>Remember, not all Android devices are guaranteed to have Bluetooth functionality.</p> +{@hide} +</BODY> +</HTML> |
