| /* |
| * Copyright 2019 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * 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.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SuppressLint; |
| import android.content.AttributionSource; |
| import android.content.Context; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.ParcelUuid; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.UUID; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * This class provides the APIs to control the Call Control profile. |
| * |
| * <p>This class provides Bluetooth Telephone Bearer Service functionality, allowing applications to |
| * expose a GATT Service based interface to control the state of the calls by remote devices such as |
| * LE audio devices. |
| * |
| * <p>BluetoothLeCallControl is a proxy object for controlling the Bluetooth Telephone Bearer |
| * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothLeCallControl |
| * proxy object. |
| * |
| * @hide |
| */ |
| public final class BluetoothLeCallControl implements BluetoothProfile { |
| private static final String TAG = "BluetoothLeCallControl"; |
| private static final boolean DBG = true; |
| private static final boolean VDBG = false; |
| |
| /** @hide */ |
| @IntDef( |
| prefix = "RESULT_", |
| value = { |
| RESULT_SUCCESS, |
| RESULT_ERROR_UNKNOWN_CALL_ID, |
| RESULT_ERROR_INVALID_URI, |
| RESULT_ERROR_APPLICATION |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Result {} |
| |
| /** |
| * Opcode write was successful. |
| * |
| * @hide |
| */ |
| public static final int RESULT_SUCCESS = 0; |
| |
| /** |
| * Unknown call Id has been used in the operation. |
| * |
| * @hide |
| */ |
| public static final int RESULT_ERROR_UNKNOWN_CALL_ID = 1; |
| |
| /** |
| * The URI provided in {@link Callback#onPlaceCallRequest} is invalid. |
| * |
| * @hide |
| */ |
| public static final int RESULT_ERROR_INVALID_URI = 2; |
| |
| /** |
| * Application internal error. |
| * |
| * @hide |
| */ |
| public static final int RESULT_ERROR_APPLICATION = 3; |
| |
| /** @hide */ |
| @IntDef( |
| prefix = "TERMINATION_REASON_", |
| value = { |
| TERMINATION_REASON_INVALID_URI, |
| TERMINATION_REASON_FAIL, |
| TERMINATION_REASON_REMOTE_HANGUP, |
| TERMINATION_REASON_SERVER_HANGUP, |
| TERMINATION_REASON_LINE_BUSY, |
| TERMINATION_REASON_NETWORK_CONGESTION, |
| TERMINATION_REASON_CLIENT_HANGUP, |
| TERMINATION_REASON_NO_SERVICE, |
| TERMINATION_REASON_NO_ANSWER |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TerminationReason {} |
| |
| /** |
| * Remote Caller ID value used to place a call was formed improperly. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_INVALID_URI = 0x00; |
| |
| /** |
| * Call fail. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_FAIL = 0x01; |
| |
| /** |
| * Remote party ended call. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_REMOTE_HANGUP = 0x02; |
| |
| /** |
| * Call ended from the server. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_SERVER_HANGUP = 0x03; |
| |
| /** |
| * Line busy. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_LINE_BUSY = 0x04; |
| |
| /** |
| * Network congestion. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_NETWORK_CONGESTION = 0x05; |
| |
| /** |
| * Client terminated. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_CLIENT_HANGUP = 0x06; |
| |
| /** |
| * No service. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_NO_SERVICE = 0x07; |
| |
| /** |
| * No answer. |
| * |
| * @hide |
| */ |
| public static final int TERMINATION_REASON_NO_ANSWER = 0x08; |
| |
| /* |
| * Flag indicating support for hold/unhold call feature. |
| * |
| * @hide |
| */ |
| public static final int CAPABILITY_HOLD_CALL = 0x00000001; |
| |
| /** |
| * Flag indicating support for joining calls feature. |
| * |
| * @hide |
| */ |
| public static final int CAPABILITY_JOIN_CALLS = 0x00000002; |
| |
| /** |
| * The template class is used to call callback functions on events from the TBS server. Callback |
| * functions are wrapped in this class and registered to the Android system during app |
| * registration. |
| * |
| * @hide |
| */ |
| public abstract static class Callback { |
| |
| private static final String TAG = "BluetoothLeCallControl.Callback"; |
| |
| /** |
| * Called when a remote client requested to accept the call. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callId The call Id requested to be accepted |
| * @hide |
| */ |
| public abstract void onAcceptCall(int requestId, @NonNull UUID callId); |
| |
| /** |
| * A remote client has requested to terminate the call. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callId The call Id requested to terminate |
| * @hide |
| */ |
| public abstract void onTerminateCall(int requestId, @NonNull UUID callId); |
| |
| /** |
| * A remote client has requested to hold the call. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callId The call Id requested to be put on hold |
| * @hide |
| */ |
| public void onHoldCall(int requestId, @NonNull UUID callId) { |
| Log.e(TAG, "onHoldCall: unimplemented, however CAPABILITY_HOLD_CALL is set!"); |
| } |
| |
| /** |
| * A remote client has requested to unhold the call. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callId The call Id requested to unhold |
| * @hide |
| */ |
| public void onUnholdCall(int requestId, @NonNull UUID callId) { |
| Log.e(TAG, "onUnholdCall: unimplemented, however CAPABILITY_HOLD_CALL is set!"); |
| } |
| |
| /** |
| * A remote client has requested to place a call. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callId The Id to be assigned for the new call |
| * @param uri The caller URI requested |
| * @hide |
| */ |
| public abstract void onPlaceCall(int requestId, @NonNull UUID callId, @NonNull String uri); |
| |
| /** |
| * A remote client has requested to join the calls. |
| * |
| * <p>An application must call {@link BluetoothLeCallControl#requestResult} to complete the |
| * request. |
| * |
| * @param requestId The Id of the request |
| * @param callIds The call Id list requested to join |
| * @hide |
| */ |
| public void onJoinCalls(int requestId, @NonNull List<UUID> callIds) { |
| Log.e(TAG, "onJoinCalls: unimplemented, however CAPABILITY_JOIN_CALLS is set!"); |
| } |
| } |
| |
| private class CallbackWrapper extends IBluetoothLeCallControlCallback.Stub { |
| |
| private final Executor mExecutor; |
| private final Callback mCallback; |
| |
| CallbackWrapper(Executor executor, Callback callback) { |
| mExecutor = executor; |
| mCallback = callback; |
| } |
| |
| @Override |
| public void onBearerRegistered(int ccid) { |
| if (mCallback != null) { |
| mCcid = ccid; |
| } else { |
| // registration timeout |
| Log.e(TAG, "onBearerRegistered: mCallback is null"); |
| } |
| } |
| |
| @Override |
| public void onAcceptCall(int requestId, ParcelUuid uuid) { |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onAcceptCall(requestId, uuid.getUuid())); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| @Override |
| public void onTerminateCall(int requestId, ParcelUuid uuid) { |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onTerminateCall(requestId, uuid.getUuid())); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| @Override |
| public void onHoldCall(int requestId, ParcelUuid uuid) { |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onHoldCall(requestId, uuid.getUuid())); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| @Override |
| public void onUnholdCall(int requestId, ParcelUuid uuid) { |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onUnholdCall(requestId, uuid.getUuid())); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| @Override |
| public void onPlaceCall(int requestId, ParcelUuid uuid, String uri) { |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onPlaceCall(requestId, uuid.getUuid(), uri)); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| |
| @Override |
| public void onJoinCalls(int requestId, List<ParcelUuid> parcelUuids) { |
| List<UUID> uuids = new ArrayList<>(); |
| for (ParcelUuid parcelUuid : parcelUuids) { |
| uuids.add(parcelUuid.getUuid()); |
| } |
| |
| final long identityToken = Binder.clearCallingIdentity(); |
| try { |
| mExecutor.execute(() -> mCallback.onJoinCalls(requestId, uuids)); |
| } finally { |
| Binder.restoreCallingIdentity(identityToken); |
| } |
| } |
| } |
| ; |
| |
| private BluetoothAdapter mAdapter; |
| private final AttributionSource mAttributionSource; |
| private int mCcid = 0; |
| private String mToken; |
| private Callback mCallback = null; |
| |
| private IBluetoothLeCallControl mService; |
| |
| /** |
| * Create a BluetoothLeCallControl proxy object for interacting with the local Bluetooth |
| * telephone bearer service. |
| */ |
| /* package */ BluetoothLeCallControl(Context context, BluetoothAdapter adapter) { |
| mAdapter = adapter; |
| mAttributionSource = mAdapter.getAttributionSource(); |
| mService = null; |
| } |
| |
| /** @hide */ |
| public void close() { |
| if (VDBG) log("close()"); |
| |
| mAdapter.closeProfileProxy(this); |
| } |
| |
| /** @hide */ |
| @Override |
| public void onServiceConnected(IBinder service) { |
| mService = IBluetoothLeCallControl.Stub.asInterface(service); |
| } |
| |
| /** @hide */ |
| @Override |
| public void onServiceDisconnected() { |
| mService = null; |
| } |
| |
| private IBluetoothLeCallControl getService() { |
| return mService; |
| } |
| |
| /** @hide */ |
| @Override |
| public BluetoothAdapter getAdapter() { |
| return mAdapter; |
| } |
| |
| /** |
| * Not supported |
| * |
| * @throws UnsupportedOperationException on every call |
| */ |
| @Override |
| public int getConnectionState(@Nullable BluetoothDevice device) { |
| throw new UnsupportedOperationException("not supported"); |
| } |
| |
| /** |
| * Not supported |
| * |
| * @throws UnsupportedOperationException on every call |
| */ |
| @Override |
| public @NonNull List<BluetoothDevice> getConnectedDevices() { |
| throw new UnsupportedOperationException("not supported"); |
| } |
| |
| /** |
| * Not supported |
| * |
| * @throws UnsupportedOperationException on every call |
| */ |
| @Override |
| @NonNull |
| public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) { |
| throw new UnsupportedOperationException("not supported"); |
| } |
| |
| /** |
| * Register Telephone Bearer exposing the interface that allows remote devices to track and |
| * control the call states. |
| * |
| * <p>This is an asynchronous call. The callback is used to notify success or failure if the |
| * function returns true. |
| * |
| * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. |
| * <!-- The UCI is a String identifier of the telephone bearer as defined at |
| * https://www.bluetooth.com/specifications/assigned-numbers/uniform-caller-identifiers |
| * (login required). --> |
| * <!-- The examples of common URI schemes can be found in |
| * https://iana.org/assignments/uri-schemes/uri-schemes.xhtml --> |
| * <!-- The Technology is an integer value. The possible values are defined at |
| * https://www.bluetooth.com/specifications/assigned-numbers (login required). |
| * --> |
| * |
| * @param uci Bearer Unique Client Identifier |
| * @param uriSchemes URI Schemes supported list |
| * @param capabilities bearer capabilities |
| * @param provider Network provider name |
| * @param technology Network technology |
| * @param executor {@link Executor} object on which callback will be executed. The Executor |
| * object is required. |
| * @param callback {@link Callback} object to which callback messages will be sent. The Callback |
| * object is required. |
| * @return true on success, false otherwise |
| * @hide |
| */ |
| @SuppressLint("ExecutorRegistration") |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public boolean registerBearer( |
| @Nullable String uci, |
| @NonNull List<String> uriSchemes, |
| int capabilities, |
| @NonNull String provider, |
| int technology, |
| @NonNull Executor executor, |
| @NonNull Callback callback) { |
| if (DBG) { |
| Log.d(TAG, "registerBearer"); |
| } |
| if (callback == null) { |
| throw new IllegalArgumentException("null parameter: " + callback); |
| } |
| if (mCcid != 0) { |
| return false; |
| } |
| |
| mToken = uci; |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return false; |
| } |
| |
| if (mCallback != null) { |
| Log.e(TAG, "Bearer can be opened only once"); |
| return false; |
| } |
| |
| mCallback = callback; |
| try { |
| CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback); |
| service.registerBearer( |
| mToken, |
| callbackWrapper, |
| uci, |
| uriSchemes, |
| capabilities, |
| provider, |
| technology, |
| mAttributionSource); |
| |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| mCallback = null; |
| return false; |
| } |
| |
| if (mCcid == 0) { |
| mCallback = null; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Unregister Telephone Bearer Service and destroy all the associated data. |
| * |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void unregisterBearer() { |
| if (DBG) { |
| Log.d(TAG, "unregisterBearer"); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| mCcid = 0; |
| mCallback = null; |
| |
| try { |
| service.unregisterBearer(mToken, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Get the Content Control ID (CCID) value. |
| * |
| * @return ccid Content Control ID value |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public int getContentControlId() { |
| return mCcid; |
| } |
| |
| /** |
| * Notify about the newly added call. |
| * |
| * <p>This shall be called as early as possible after the call has been added. |
| * |
| * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. |
| * |
| * @param call Newly added call |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void onCallAdded(@NonNull BluetoothLeCall call) { |
| if (DBG) { |
| Log.d(TAG, "onCallAdded: call=" + call); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| try { |
| service.callAdded(mCcid, call, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Notify about the removed call. |
| * |
| * <p>This shall be called as early as possible after the call has been removed. |
| * |
| * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. |
| * |
| * @param callId The Id of a call that has been removed |
| * @param reason Call termination reason |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void onCallRemoved(@NonNull UUID callId, @TerminationReason int reason) { |
| if (DBG) { |
| Log.d(TAG, "callRemoved: callId=" + callId); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| try { |
| service.callRemoved(mCcid, new ParcelUuid(callId), reason, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Notify the call state change |
| * |
| * <p>This shall be called as early as possible after the state of the call has changed. |
| * |
| * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. |
| * |
| * @param callId The call Id that state has been changed |
| * @param state Call state |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void onCallStateChanged(@NonNull UUID callId, @BluetoothLeCall.State int state) { |
| if (DBG) { |
| Log.d(TAG, "callStateChanged: callId=" + callId + " state=" + state); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| try { |
| service.callStateChanged(mCcid, new ParcelUuid(callId), state, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Provide the current calls list |
| * |
| * <p>This function must be invoked after registration if application has any calls. |
| * |
| * @param calls current calls list |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void currentCallsList(@NonNull List<BluetoothLeCall> calls) { |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| try { |
| service.currentCallsList(mCcid, calls, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Provide the network current status |
| * |
| * <p>This function must be invoked on change of network state. |
| * |
| * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. |
| * <!-- The Technology is an integer value. The possible values are defined at |
| * https://www.bluetooth.com/specifications/assigned-numbers (login required). |
| * --> |
| * |
| * @param provider Network provider name |
| * @param technology Network technology |
| * @hide |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void networkStateChanged(@NonNull String provider, int technology) { |
| if (DBG) { |
| Log.d(TAG, "networkStateChanged: provider=" + provider + ", technology=" + technology); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| try { |
| service.networkStateChanged(mCcid, provider, technology, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| /** |
| * Send a response to a call control request to a remote device. |
| * |
| * <p>This function must be invoked in when a request is received by one of these callback |
| * methods: |
| * |
| * <ul> |
| * <li>{@link Callback#onAcceptCall} |
| * <li>{@link Callback#onTerminateCall} |
| * <li>{@link Callback#onHoldCall} |
| * <li>{@link Callback#onUnholdCall} |
| * <li>{@link Callback#onPlaceCall} |
| * <li>{@link Callback#onJoinCalls} |
| * </ul> |
| * |
| * @param requestId The ID of the request that was received with the callback |
| * @param result The result of the request to be sent to the remote devices |
| */ |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) |
| public void requestResult(int requestId, @Result int result) { |
| if (DBG) { |
| Log.d(TAG, "requestResult: requestId=" + requestId + " result=" + result); |
| } |
| if (mCcid == 0) { |
| return; |
| } |
| |
| final IBluetoothLeCallControl service = getService(); |
| if (service == null) { |
| Log.w(TAG, "Proxy not attached to service"); |
| return; |
| } |
| |
| try { |
| service.requestResult(mCcid, requestId, result, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| private static void log(String msg) { |
| Log.d(TAG, msg); |
| } |
| } |