New public APIs for BluetoothA2dp and BluetoothHeadset profiles.

Change-Id: I1cc4b109542dfd62473cb95797c8c3d0d15725f4
diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java
index 7e5f858..d308a5c 100644
--- a/core/java/android/bluetooth/BluetoothA2dp.java
+++ b/core/java/android/bluetooth/BluetoothA2dp.java
@@ -18,88 +18,104 @@
 
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
-import android.server.BluetoothA2dpService;
 import android.content.Context;
-import android.os.ServiceManager;
-import android.os.RemoteException;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.server.BluetoothA2dpService;
 import android.util.Log;
 
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.Set;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Set;
+
 
 /**
- * Public API for controlling the Bluetooth A2DP Profile Service.
+ * This class provides the public APIs to control the Bluetooth A2DP
+ * profile.
  *
- * BluetoothA2dp is a proxy object for controlling the Bluetooth A2DP
- * Service via IPC.
+ *<p>BluetoothA2dp is a proxy object for controlling the Bluetooth A2DP
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothA2dp proxy object.
  *
- * Creating a BluetoothA2dp object will initiate a binding with the
- * BluetoothHeadset service. Users of this object should call close() when they
- * are finished, so that this proxy object can unbind from the service.
- *
- * Currently the BluetoothA2dp service runs in the system server and this
- * proxy object will be immediately bound to the service on construction.
- *
- * Currently this class provides methods to connect to A2DP audio sinks.
- *
- * @hide
+ * <p> Android only supports one connected Bluetooth A2dp device at a time.
+ * Each method is protected with its appropriate permission.
  */
-public final class BluetoothA2dp {
+public final class BluetoothA2dp implements BluetoothProfile {
     private static final String TAG = "BluetoothA2dp";
     private static final boolean DBG = false;
 
-    /** int extra for ACTION_SINK_STATE_CHANGED */
-    public static final String EXTRA_SINK_STATE =
-        "android.bluetooth.a2dp.extra.SINK_STATE";
-    /** int extra for ACTION_SINK_STATE_CHANGED */
-    public static final String EXTRA_PREVIOUS_SINK_STATE =
-        "android.bluetooth.a2dp.extra.PREVIOUS_SINK_STATE";
-
-    /** Indicates the state of an A2DP audio sink has changed.
-     * This intent will always contain EXTRA_SINK_STATE,
-     * EXTRA_PREVIOUS_SINK_STATE and BluetoothDevice.EXTRA_DEVICE
-     * extras.
+    /**
+     * Intent used to broadcast the change in connection state of the A2DP
+     * profile.
+     *
+     * <p>This intent will have 3 extras:
+     * {@link #EXTRA_STATE} - The current state of the profile.
+     * {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile
+     * {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+     *
+     * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+     * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+     * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+     *
+     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
-    public static final String ACTION_SINK_STATE_CHANGED =
-        "android.bluetooth.a2dp.action.SINK_STATE_CHANGED";
+    public static final String ACTION_CONNECTION_STATE_CHANGED =
+        "android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED";
 
-    public static final int STATE_DISCONNECTED = 0;
-    public static final int STATE_CONNECTING   = 1;
-    public static final int STATE_CONNECTED    = 2;
-    public static final int STATE_DISCONNECTING = 3;
-    /** Playing implies connected */
-    public static final int STATE_PLAYING    = 4;
+    /**
+     * Intent used to broadcast the change in the Playing state of the A2DP
+     * profile.
+     *
+     * <p>This intent will have 3 extras:
+     * {@link #EXTRA_STATE} - The current state of the profile.
+     * {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile
+     * {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+     *
+     * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+     * {@link #STATE_PLAYING}, {@link #STATE_NOT_PLAYING},
+     *
+     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_PLAYING_STATE_CHANGED =
+        "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED";
 
-    /** Default priority for a2dp devices that we try to auto-connect
-     * and allow incoming connections */
-    public static final int PRIORITY_AUTO_CONNECT = 1000;
-    /** Default priority for a2dp devices that should allow incoming
-     * connections */
-    public static final int PRIORITY_ON = 100;
-    /** Default priority for a2dp devices that should not allow incoming
-     * connections */
-    public static final int PRIORITY_OFF = 0;
-    /** Default priority when not set or when the device is unpaired */
-    public static final int PRIORITY_UNDEFINED = -1;
+    /**
+     * A2DP sink device is streaming music. This state can be one of
+     * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+     * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+     */
+    public static final int STATE_PLAYING   =  10;
 
-    private final IBluetoothA2dp mService;
-    private final Context mContext;
+    /**
+     * A2DP sink device is NOT streaming music. This state can be one of
+     * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+     * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+     */
+    public static final int STATE_NOT_PLAYING   =  11;
+
+    private ServiceListener mServiceListener;
+    private IBluetoothA2dp mService;
+    private BluetoothAdapter mAdapter;
 
     /**
      * Create a BluetoothA2dp proxy object for interacting with the local
      * Bluetooth A2DP service.
-     * @param c Context
+     *
      */
-    public BluetoothA2dp(Context c) {
-        mContext = c;
-
+    /*package*/ BluetoothA2dp(Context mContext, ServiceListener l) {
         IBinder b = ServiceManager.getService(BluetoothA2dpService.BLUETOOTH_A2DP_SERVICE);
+        mServiceListener = l;
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
         if (b != null) {
             mService = IBluetoothA2dp.Stub.asInterface(b);
+            if (mServiceListener != null) {
+                mServiceListener.onServiceConnected(BluetoothProfile.A2DP, this);
+            }
         } else {
             Log.w(TAG, "Bluetooth A2DP service not available!");
 
@@ -109,167 +125,222 @@
         }
     }
 
-    /** Initiate a connection to an A2DP sink.
-     *  Listen for SINK_STATE_CHANGED_ACTION to find out when the
-     *  connection is completed.
-     *  @param device Remote BT device.
-     *  @return false on immediate error, true otherwise
-     *  @hide
+    /**
+     * {@inheritDoc}
+     * @hide
      */
-    public boolean connectSink(BluetoothDevice device) {
-        if (DBG) log("connectSink(" + device + ")");
-        try {
-            return mService.connectSink(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return false;
+    public boolean connect(BluetoothDevice device) {
+        if (DBG) log("connect(" + device + ")");
+        if (mService != null && isEnabled() &&
+            isValidDevice(device)) {
+            try {
+                return mService.connect(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
         }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
     }
 
-    /** Initiate disconnect from an A2DP sink.
-     *  Listen for SINK_STATE_CHANGED_ACTION to find out when
-     *  disconnect is completed.
-     *  @param device Remote BT device.
-     *  @return false on immediate error, true otherwise
-     *  @hide
+    /**
+     * {@inheritDoc}
+     * @hide
      */
-    public boolean disconnectSink(BluetoothDevice device) {
-        if (DBG) log("disconnectSink(" + device + ")");
-        try {
-            return mService.disconnectSink(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return false;
+    public boolean disconnect(BluetoothDevice device) {
+        if (DBG) log("disconnect(" + device + ")");
+        if (mService != null && isEnabled() &&
+            isValidDevice(device)) {
+            try {
+                return mService.disconnect(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
         }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
     }
 
-    /** Initiate suspend from an A2DP sink.
-     *  Listen for SINK_STATE_CHANGED_ACTION to find out when
-     *  suspend is completed.
-     *  @param device Remote BT device.
-     *  @return false on immediate error, true otherwise
-     *  @hide
+    /**
+     * {@inheritDoc}
+     */
+    public Set<BluetoothDevice> getConnectedDevices() {
+        if (DBG) log("getConnectedDevices()");
+        if (mService != null && isEnabled()) {
+            try {
+                return toDeviceSet(mService.getConnectedDevices());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return toDeviceSet(new BluetoothDevice[0]);
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return toDeviceSet(new BluetoothDevice[0]);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Set<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+        if (DBG) log("getDevicesMatchingStates()");
+        if (mService != null && isEnabled()) {
+            try {
+                return toDeviceSet(mService.getDevicesMatchingConnectionStates(states));
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return toDeviceSet(new BluetoothDevice[0]);
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return toDeviceSet(new BluetoothDevice[0]);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int getConnectionState(BluetoothDevice device) {
+        if (DBG) log("getState(" + device + ")");
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            try {
+                return mService.getConnectionState(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return BluetoothProfile.STATE_DISCONNECTED;
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return BluetoothProfile.STATE_DISCONNECTED;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @hide
+     */
+    public boolean setPriority(BluetoothDevice device, int priority) {
+        if (DBG) log("setPriority(" + device + ", " + priority + ")");
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            if (priority != BluetoothProfile.PRIORITY_OFF &&
+                priority != BluetoothProfile.PRIORITY_ON) {
+              return false;
+            }
+            try {
+                return mService.setPriority(device, priority);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @hide
+     */
+    public int getPriority(BluetoothDevice device) {
+        if (DBG) log("getPriority(" + device + ")");
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            try {
+                return mService.getPriority(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return BluetoothProfile.PRIORITY_OFF;
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return BluetoothProfile.PRIORITY_OFF;
+    }
+
+    /**
+     * Check if A2DP profile is streaming music.
+     *
+     * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
+     *
+     * @param device BluetoothDevice device
+     */
+    public boolean isA2dpPlaying(BluetoothDevice device) {
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            try {
+                return mService.isA2dpPlaying(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
+        }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
+    }
+
+    /**
+     * Initiate suspend from an A2DP sink.
+     *
+     * <p> This API will return false in scenarios like the A2DP
+     * device is not in connected state etc. When this API returns,
+     * true, it is guaranteed that {@link #ACTION_SINK_STATE_CHANGED}
+     * intent will be broadcasted with the state. Users can get the
+     * state of the A2DP device from this intent.
+     *
+     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+     *
+     * @param device Remote A2DP sink
+     * @return false on immediate error,
+     *               true otherwise
+     * @hide
      */
     public boolean suspendSink(BluetoothDevice device) {
-        try {
-            return mService.suspendSink(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return false;
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            try {
+                return mService.suspendSink(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
         }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
     }
 
-    /** Initiate resume from an suspended A2DP sink.
-     *  Listen for SINK_STATE_CHANGED_ACTION to find out when
-     *  resume is completed.
-     *  @param device Remote BT device.
-     *  @return false on immediate error, true otherwise
-     *  @hide
+    /**
+     * Initiate resume from a suspended A2DP sink.
+     *
+     * <p> This API will return false in scenarios like the A2DP
+     * device is not in suspended state etc. When this API returns,
+     * true, it is guaranteed that {@link #ACTION_SINK_STATE_CHANGED}
+     * intent will be broadcasted with the state. Users can get the
+     * state of the A2DP device from this intent.
+     *
+     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+     *
+     * @param device Remote A2DP sink
+     * @return false on immediate error,
+     *               true otherwise
+     * @hide
      */
     public boolean resumeSink(BluetoothDevice device) {
-        try {
-            return mService.resumeSink(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return false;
+        if (mService != null && isEnabled()
+            && isValidDevice(device)) {
+            try {
+                return mService.resumeSink(device);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+                return false;
+            }
         }
+        if (mService == null) Log.w(TAG, "Proxy not attached to service");
+        return false;
     }
 
-    /** Check if a specified A2DP sink is connected.
-     *  @param device Remote BT device.
-     *  @return True if connected (or playing), false otherwise and on error.
-     *  @hide
-     */
-    public boolean isSinkConnected(BluetoothDevice device) {
-        if (DBG) log("isSinkConnected(" + device + ")");
-        int state = getSinkState(device);
-        return state == STATE_CONNECTED || state == STATE_PLAYING;
-    }
-
-    /** Check if any A2DP sink is connected.
-     * @return a unmodifiable set of connected A2DP sinks, or null on error.
-     * @hide
-     */
-    public Set<BluetoothDevice> getConnectedSinks() {
-        if (DBG) log("getConnectedSinks()");
-        try {
-            return Collections.unmodifiableSet(
-                    new HashSet<BluetoothDevice>(Arrays.asList(mService.getConnectedSinks())));
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return null;
-        }
-    }
-
-    /** Check if any A2DP sink is in Non Disconnected state
-     * i.e playing, connected, connecting, disconnecting.
-     * @return a unmodifiable set of connected A2DP sinks, or null on error.
-     * @hide
-     */
-    public Set<BluetoothDevice> getNonDisconnectedSinks() {
-        if (DBG) log("getNonDisconnectedSinks()");
-        try {
-            return Collections.unmodifiableSet(
-                    new HashSet<BluetoothDevice>(Arrays.asList(mService.getNonDisconnectedSinks())));
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return null;
-        }
-    }
-
-    /** Get the state of an A2DP sink
-     *  @param device Remote BT device.
-     *  @return State code, one of STATE_
-     *  @hide
-     */
-    public int getSinkState(BluetoothDevice device) {
-        if (DBG) log("getSinkState(" + device + ")");
-        try {
-            return mService.getSinkState(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return BluetoothA2dp.STATE_DISCONNECTED;
-        }
-    }
-
-    /**
-     * Set priority of a2dp sink.
-     * Priority is a non-negative integer. By default paired sinks will have
-     * a priority of PRIORITY_AUTO, and unpaired headset PRIORITY_NONE (0).
-     * Sinks with priority greater than zero will accept incoming connections
-     * (if no sink is currently connected).
-     * Priority for unpaired sink must be PRIORITY_NONE.
-     * @param device Paired sink
-     * @param priority Integer priority, for example PRIORITY_AUTO or
-     *                 PRIORITY_NONE
-     * @return true if priority is set, false on error
-     */
-    public boolean setSinkPriority(BluetoothDevice device, int priority) {
-        if (DBG) log("setSinkPriority(" + device + ", " + priority + ")");
-        try {
-            return mService.setSinkPriority(device, priority);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return false;
-        }
-    }
-
-    /**
-     * Get priority of a2dp sink.
-     * @param device Sink
-     * @return non-negative priority, or negative error code on error.
-     */
-    public int getSinkPriority(BluetoothDevice device) {
-        if (DBG) log("getSinkPriority(" + device + ")");
-        try {
-            return mService.getSinkPriority(device);
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-            return PRIORITY_OFF;
-        }
-    }
-
-    /** Helper for converting a state to a string.
+     /**
+     * Helper for converting a state to a string.
+     *
      * For debug use only - strings are not internationalized.
      * @hide
      */
@@ -285,12 +356,31 @@
             return "disconnecting";
         case STATE_PLAYING:
             return "playing";
+        case STATE_NOT_PLAYING:
+          return "not playing";
         default:
             return "<unknown state " + state + ">";
         }
     }
 
+    private boolean isEnabled() {
+       if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+       return false;
+    }
+
+    private boolean isValidDevice(BluetoothDevice device) {
+       if (device == null) return false;
+
+       if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+       return false;
+    }
+
+    private Set<BluetoothDevice> toDeviceSet(BluetoothDevice[] devices) {
+       return Collections.unmodifiableSet(
+          new HashSet<BluetoothDevice>(Arrays.asList(devices)));
+    }
+
     private static void log(String msg) {
-        Log.d(TAG, msg);
+      Log.d(TAG, msg);
     }
 }