/* * 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. * *

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. * *

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. * *

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. * *

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. * *

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. * *

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. * *

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. * *

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 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 parcelUuids) { List 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 getConnectedDevices() { throw new UnsupportedOperationException("not supported"); } /** * Not supported * * @throws UnsupportedOperationException on every call */ @Override @NonNull public List 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. * *

This is an asynchronous call. The callback is used to notify success or failure if the * function returns true. * *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. * * * * * @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 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. * *

This shall be called as early as possible after the call has been added. * *

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. * *

This shall be called as early as possible after the call has been removed. * *

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 * *

This shall be called as early as possible after the state of the call has changed. * *

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 * *

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 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 * *

This function must be invoked on change of network state. * *

Requires {@link android.Manifest.permission#BLUETOOTH} permission. * * * @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. * *

This function must be invoked in when a request is received by one of these callback * methods: * *

* * @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); } }