gtbs: Add Generic Telephone Bearer Service implementation

This adds Generic Telephone Bearer Service (GTBS) implementation
and exposes API that allows other TBS instances to be registered
by applications which want to expose their call control interfaces
over Bluetooth.

The GTBS aggregates call states of all the registered TBSes.
Each Call from GATT Client perspective is identified by a call
index assigned by TbsServerService, which is unique across TBS
instances and GTBS. From an application perspective, a call is
identified by the BluetoothTbsCall instance which has a unique UUID.

Bug: 150670922
Tag: #feature
Sponsor: jpawlowski@
test: Test: atest BluetoothInstrumentationTests
Change-Id: I652a40b3f1ce01fc57f0afea451ed1d39781ed36
diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml
index 9083762..b84bdf9 100644
--- a/android/app/AndroidManifest.xml
+++ b/android/app/AndroidManifest.xml
@@ -449,6 +449,15 @@
                 <action android:name="android.bluetooth.IBluetoothCsipSetCoordinator" />
             </intent-filter>
         </service>
+        <service
+            android:process="@string/process"
+            android:name = ".tbs.TbsService"
+            android:enabled="@bool/profile_supported_le_call_control"
+            android:exported = "true">
+            <intent-filter>
+                <action android:name="android.bluetooth.IBluetoothLeCallControl" />
+            </intent-filter>
+        </service>
         <!-- Authenticator for PBAP account. -->
         <service android:process="@string/process"
              android:name=".pbapclient.AuthenticationService"
diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml
index 1b0a315..a34113d 100644
--- a/android/app/res/values/config.xml
+++ b/android/app/res/values/config.xml
@@ -36,6 +36,7 @@
     <bool name="profile_supported_vc">true</bool>
     <bool name="profile_supported_mcp_server">true</bool>
     <bool name="profile_supported_csip_set_coordinator">true</bool>
+    <bool name="profile_supported_le_call_control">true</bool>
 
     <!-- If true, we will require location to be enabled on the device to
          fire Bluetooth LE scan result callbacks in addition to having one
diff --git a/android/app/src/com/android/bluetooth/btservice/Config.java b/android/app/src/com/android/bluetooth/btservice/Config.java
index b26266a..4c1eec3 100644
--- a/android/app/src/com/android/bluetooth/btservice/Config.java
+++ b/android/app/src/com/android/bluetooth/btservice/Config.java
@@ -49,6 +49,7 @@
 import com.android.bluetooth.pbapclient.PbapClientService;
 import com.android.bluetooth.sap.SapService;
 import com.android.bluetooth.vc.VolumeControlService;
+import com.android.bluetooth.tbs.TbsService;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -121,6 +122,8 @@
                     (1 << BluetoothProfile.VOLUME_CONTROL)),
             new ProfileConfig(McpService.class, R.bool.profile_supported_mcp_server,
                     (1 << BluetoothProfile.MCP_SERVER)),
+            new ProfileConfig(TbsService.class, R.bool.profile_supported_le_call_control,
+                    (1 << BluetoothProfile.LE_CALL_CONTROL)),
             new ProfileConfig(HearingAidService.class,
                     com.android.internal.R.bool.config_hearing_aid_profile_supported,
                     (1 << BluetoothProfile.HEARING_AID)),
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index 44de31f..8ac06d0 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -119,6 +119,9 @@
             case BluetoothProfile.CSIP_SET_COORDINATOR:
                 profileConnectionPolicies.csip_set_coordinator_connection_policy = connectionPolicy;
                 break;
+            case BluetoothProfile.LE_CALL_CONTROL:
+                profileConnectionPolicies.le_call_control_connection_policy = connectionPolicy;
+                break;
             default:
                 throw new IllegalArgumentException("invalid profile " + profile);
         }
@@ -156,6 +159,8 @@
                 return profileConnectionPolicies.volume_control_connection_policy;
             case BluetoothProfile.CSIP_SET_COORDINATOR:
                 return profileConnectionPolicies.csip_set_coordinator_connection_policy;
+            case BluetoothProfile.LE_CALL_CONTROL:
+                return profileConnectionPolicies.le_call_control_connection_policy;
         }
         return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
     }
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
index b44bc31..3a12015 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -33,7 +33,7 @@
 /**
  * MetadataDatabase is a Room database stores Bluetooth persistence data
  */
-@Database(entities = {Metadata.class}, version = 108)
+@Database(entities = {Metadata.class}, version = 109)
 public abstract class MetadataDatabase extends RoomDatabase {
     /**
      * The metadata database file name
@@ -61,6 +61,7 @@
                 .addMigrations(MIGRATION_105_106)
                 .addMigrations(MIGRATION_106_107)
                 .addMigrations(MIGRATION_107_108)
+                .addMigrations(MIGRATION_108_109)
                 .allowMainThreadQueries()
                 .build();
     }
@@ -388,4 +389,22 @@
             }
         }
     };
+
+    @VisibleForTesting
+    static final Migration MIGRATION_108_109 = new Migration(108, 109) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+            try {
+                database.execSQL(
+                        "ALTER TABLE metadata ADD COLUMN `le_call_control_connection_policy` "
+                        + "INTEGER DEFAULT 100");
+            } catch (SQLException ex) {
+                // Check if user has new schema, but is just missing the version update
+                Cursor cursor = database.query("SELECT * FROM metadata");
+                if (cursor == null || cursor.getColumnIndex("le_call_control_connection_policy") == -1) {
+                    throw ex;
+                }
+            }
+        }
+    };
 }
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
index 879b263..a9a75c7 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
@@ -38,6 +38,7 @@
     public int le_audio_connection_policy;
     public int volume_control_connection_policy;
     public int csip_set_coordinator_connection_policy;
+    public int le_call_control_connection_policy;
 
     ProfilePrioritiesEntity() {
         a2dp_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
@@ -55,6 +56,7 @@
         le_audio_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
         volume_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
         csip_set_coordinator_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+        le_call_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
     }
 
     public String toString() {
@@ -73,7 +75,8 @@
                 .append("|SAP=").append(sap_connection_policy)
                 .append("|HEARING_AID=").append(hearing_aid_connection_policy)
                 .append("|LE_AUDIO=").append(le_audio_connection_policy)
-                .append("|VOLUME_CONTROL=").append(volume_control_connection_policy);
+                .append("|VOLUME_CONTROL=").append(volume_control_connection_policy)
+                .append("|LE_CALL_CONTROL=").append(le_call_control_connection_policy);
 
         return builder.toString();
     }
diff --git a/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
new file mode 100644
index 0000000..a68af61
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import java.util.List;
+
+/**
+ * A proxy class that facilitates testing of the TbsService class.
+ *
+ * This is necessary due to the "final" attribute of the BluetoothGattServer class. In order to test
+ * the correct functioning of the TbsService class, the final class must be put into a container
+ * that can be mocked correctly.
+ */
+public class BluetoothGattServerProxy {
+
+    private final Context mContext;
+    private BluetoothManager mBluetoothManager;
+    private BluetoothGattServer mBluetoothGattServer;
+
+    public BluetoothGattServerProxy(Context context) {
+        mContext = context;
+        mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+    }
+
+    public boolean open(BluetoothGattServerCallback callback) {
+        mBluetoothGattServer = mBluetoothManager.openGattServer(mContext, callback);
+        return (mBluetoothGattServer != null);
+    }
+
+    public void close() {
+        if (mBluetoothGattServer == null) {
+            return;
+        }
+        mBluetoothGattServer.close();
+        mBluetoothGattServer = null;
+    }
+
+    public boolean addService(BluetoothGattService service) {
+        return mBluetoothGattServer.addService(service);
+    }
+
+    public boolean sendResponse(BluetoothDevice device, int requestId, int status, int offset,
+            byte[] value) {
+        return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
+    }
+
+    public boolean notifyCharacteristicChanged(BluetoothDevice device,
+            BluetoothGattCharacteristic characteristic, boolean confirm) {
+        return mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+    }
+
+    public List<BluetoothDevice> getConnectedDevices() {
+        return mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/tbs/BluetoothLeCallControlProxy.java b/android/app/src/com/android/bluetooth/tbs/BluetoothLeCallControlProxy.java
new file mode 100644
index 0000000..ca0534b
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/BluetoothLeCallControlProxy.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.bluetooth.BluetoothLeCallControl;
+import android.bluetooth.BluetoothLeCall;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+/*
+ * A proxy class that facilitates testing of the BluetoothInCallService class.
+ *
+ * This is necessary due to the "final" attribute of the BluetoothLeCallControl class. In order to test the
+ * correct functioning of the BluetoothInCallService class, the final class must be put into a
+ * container that can be mocked correctly.
+ */
+public class BluetoothLeCallControlProxy {
+
+    private BluetoothLeCallControl mBluetoothLeCallControl;
+
+    public BluetoothLeCallControlProxy(BluetoothLeCallControl tbs) {
+        mBluetoothLeCallControl = tbs;
+    }
+
+    public boolean registerBearer(String uci, List<String> uriSchemes, int featureFlags,
+            String provider, int technology, Executor executor, BluetoothLeCallControl.Callback callback) {
+        return mBluetoothLeCallControl.registerBearer(uci, uriSchemes, featureFlags, provider, technology,
+                executor, callback);
+    }
+
+    public void unregisterBearer() {
+        mBluetoothLeCallControl.unregisterBearer();
+    }
+
+    public int getContentControlId() {
+        return mBluetoothLeCallControl.getContentControlId();
+    }
+
+    public void requestResult(int requestId, int result) {
+        mBluetoothLeCallControl.requestResult(requestId, result);
+    }
+
+    public void onCallAdded(BluetoothLeCall call) {
+        mBluetoothLeCallControl.onCallAdded(call);
+    }
+
+    public void onCallRemoved(UUID callId, int reason) {
+        mBluetoothLeCallControl.onCallRemoved(callId, reason);
+    }
+
+    public void onCallStateChanged(UUID callId, int state) {
+        mBluetoothLeCallControl.onCallStateChanged(callId, state);
+    }
+
+    public void currentCallsList(List<BluetoothLeCall> calls) {
+        mBluetoothLeCallControl.currentCallsList(calls);
+    }
+
+    public void networkStateChanged(String providerName, int technology) {
+        mBluetoothLeCallControl.networkStateChanged(providerName, technology);
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsCall.java b/android/app/src/com/android/bluetooth/tbs/TbsCall.java
new file mode 100644
index 0000000..d6275da
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/TbsCall.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.bluetooth.BluetoothLeCall;
+
+import java.util.UUID;
+
+public class TbsCall {
+
+    public static final int INDEX_UNASSIGNED = 0x00;
+    public static final int INDEX_MIN = 0x01;
+    public static final int INDEX_MAX = 0xFF;
+
+    private int mState;
+    private String mUri;
+    private int mFlags;
+    private String mFriendlyName;
+
+    private TbsCall(int state, String uri, int flags, String friendlyName) {
+        mState = state;
+        mUri = uri;
+        mFlags = flags;
+        mFriendlyName = friendlyName;
+    }
+
+    public static TbsCall create(BluetoothLeCall call) {
+        return new TbsCall(call.getState(), call.getUri(), call.getCallFlags(),
+                call.getFriendlyName());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        TbsCall that = (TbsCall) o;
+        // check the state only
+        return mState == that.mState;
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public void setState(int state) {
+        mState = state;
+    }
+
+    public String getUri() {
+        return mUri;
+    }
+
+    public int getFlags() {
+        return mFlags;
+    }
+
+    public boolean isIncoming() {
+        return (mFlags & BluetoothLeCall.FLAG_OUTGOING_CALL) == 0;
+    }
+
+    public String getFriendlyName() {
+        return mFriendlyName;
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
new file mode 100644
index 0000000..e3845c8
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
@@ -0,0 +1,746 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothLeCall;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+public class TbsGatt {
+
+    private static final String TAG = "TbsGatt";
+    private static final boolean DBG = true;
+
+    private static final String UUID_PREFIX = "0000";
+    private static final String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
+
+    /* TBS assigned uuid's */
+    @VisibleForTesting
+    static final UUID UUID_TBS = makeUuid("184B");
+    @VisibleForTesting
+    static final UUID UUID_GTBS = makeUuid("184C");
+    @VisibleForTesting
+    static final UUID UUID_BEARER_PROVIDER_NAME = makeUuid("2BB3");
+    @VisibleForTesting
+    static final UUID UUID_BEARER_UCI = makeUuid("2BB4");
+    @VisibleForTesting
+    static final UUID UUID_BEARER_TECHNOLOGY = makeUuid("2BB5");
+    @VisibleForTesting
+    static final UUID UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST = makeUuid("2BB6");
+    @VisibleForTesting
+    static final UUID UUID_BEARER_LIST_CURRENT_CALLS = makeUuid("2BB9");
+    @VisibleForTesting
+    static final UUID UUID_CONTENT_CONTROL_ID = makeUuid("2BBA");
+    @VisibleForTesting
+    static final UUID UUID_STATUS_FLAGS = makeUuid("2BBB");
+    @VisibleForTesting
+    static final UUID UUID_CALL_STATE = makeUuid("2BBD");
+    @VisibleForTesting
+    static final UUID UUID_CALL_CONTROL_POINT = makeUuid("2BBE");
+    @VisibleForTesting
+    static final UUID UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES = makeUuid("2BBF");
+    @VisibleForTesting
+    static final UUID UUID_TERMINATION_REASON = makeUuid("2BC0");
+    @VisibleForTesting
+    static final UUID UUID_INCOMING_CALL = makeUuid("2BC1");
+    @VisibleForTesting
+    static final UUID UUID_CALL_FRIENDLY_NAME = makeUuid("2BC2");
+    @VisibleForTesting
+    static final UUID UUID_CLIENT_CHARACTERISTIC_CONFIGURATION = makeUuid("2902");
+
+    @VisibleForTesting
+    static final int STATUS_FLAG_INBAND_RINGTONE_ENABLED = 0x0001;
+    @VisibleForTesting
+    static final int STATUS_FLAG_SILENT_MODE_ENABLED = 0x0002;
+
+    @VisibleForTesting
+    static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD = 0x0001;
+    @VisibleForTesting
+    static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN = 0x0002;
+
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_ACCEPT = 0x00;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_TERMINATE = 0x01;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD = 0x02;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE = 0x03;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_ORIGINATE = 0x04;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_OPCODE_JOIN = 0x05;
+
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_SUCCESS = 0x00;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED = 0x01;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE = 0x02;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX = 0x03;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_STATE_MISMATCH = 0x04;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_LACK_OF_RESOURCES = 0x05;
+    @VisibleForTesting
+    public static final int CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI = 0x06;
+
+    private final Context mContext;
+    private final GattCharacteristic mBearerProviderNameCharacteristic;
+    private final GattCharacteristic mBearerUciCharacteristic;
+    private final GattCharacteristic mBearerTechnologyCharacteristic;
+    private final GattCharacteristic mBearerUriSchemesSupportedListCharacteristic;
+    private final GattCharacteristic mBearerListCurrentCallsCharacteristic;
+    private final GattCharacteristic mContentControlIdCharacteristic;
+    private final GattCharacteristic mStatusFlagsCharacteristic;
+    private final GattCharacteristic mCallStateCharacteristic;
+    private final CallControlPointCharacteristic mCallControlPointCharacteristic;
+    private final GattCharacteristic mCallControlPointOptionalOpcodesCharacteristic;
+    private final GattCharacteristic mTerminationReasonCharacteristic;
+    private final GattCharacteristic mIncomingCallCharacteristic;
+    private final GattCharacteristic mCallFriendlyNameCharacteristic;
+    private BluetoothGattServerProxy mBluetoothGattServer;
+    private Callback mCallback;
+
+    public static abstract class Callback {
+
+        public abstract void onServiceAdded(boolean success);
+
+        public abstract void onCallControlPointRequest(BluetoothDevice device, int opcode,
+                byte[] args);
+    }
+
+    TbsGatt(Context context) {
+        mContext = context;
+        mBearerProviderNameCharacteristic = new GattCharacteristic(UUID_BEARER_PROVIDER_NAME,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mBearerUciCharacteristic =
+                new GattCharacteristic(UUID_BEARER_UCI, BluetoothGattCharacteristic.PROPERTY_READ,
+                        BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mBearerTechnologyCharacteristic = new GattCharacteristic(UUID_BEARER_TECHNOLOGY,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mBearerUriSchemesSupportedListCharacteristic =
+                new GattCharacteristic(UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST,
+                        BluetoothGattCharacteristic.PROPERTY_READ
+                                | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                        BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mBearerListCurrentCallsCharacteristic =
+                new GattCharacteristic(UUID_BEARER_LIST_CURRENT_CALLS,
+                        BluetoothGattCharacteristic.PROPERTY_READ
+                                | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                        BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mContentControlIdCharacteristic = new GattCharacteristic(UUID_CONTENT_CONTROL_ID,
+                BluetoothGattCharacteristic.PROPERTY_READ,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mStatusFlagsCharacteristic = new GattCharacteristic(UUID_STATUS_FLAGS,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mCallStateCharacteristic = new GattCharacteristic(UUID_CALL_STATE,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mCallControlPointCharacteristic = new CallControlPointCharacteristic();
+        mCallControlPointOptionalOpcodesCharacteristic = new GattCharacteristic(
+                UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES, BluetoothGattCharacteristic.PROPERTY_READ,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mTerminationReasonCharacteristic = new GattCharacteristic(UUID_TERMINATION_REASON,
+                BluetoothGattCharacteristic.PROPERTY_NOTIFY, 0);
+        mIncomingCallCharacteristic = new GattCharacteristic(UUID_INCOMING_CALL,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mCallFriendlyNameCharacteristic = new GattCharacteristic(UUID_CALL_FRIENDLY_NAME,
+                BluetoothGattCharacteristic.PROPERTY_READ
+                        | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+                BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
+        mBluetoothGattServer = null;
+    }
+
+    @VisibleForTesting
+    void setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy) {
+        mBluetoothGattServer = proxy;
+    }
+
+    public boolean init(int ccid, String uci, List<String> uriSchemes,
+            boolean isLocalHoldOpcodeSupported, boolean isJoinOpcodeSupported, String providerName,
+            int technology, Callback callback) {
+        mBearerProviderNameCharacteristic.setValue(providerName);
+        mBearerTechnologyCharacteristic.setValue(new byte[] {(byte) (technology & 0xFF)});
+        mBearerUciCharacteristic.setValue(uci);
+        setBearerUriSchemesSupportedList(uriSchemes);
+        mContentControlIdCharacteristic.setValue(ccid, BluetoothGattCharacteristic.FORMAT_UINT8, 0);
+        setCallControlPointOptionalOpcodes(isLocalHoldOpcodeSupported, isJoinOpcodeSupported);
+        mStatusFlagsCharacteristic.setValue(0, BluetoothGattCharacteristic.FORMAT_UINT16, 0);
+        mCallback = callback;
+
+        if (mBluetoothGattServer == null) {
+            mBluetoothGattServer = new BluetoothGattServerProxy(mContext);
+        }
+
+        if (!mBluetoothGattServer.open(mGattServerCallback)) {
+            Log.e(TAG, " Could not open Gatt server");
+            return false;
+        }
+
+        BluetoothGattService gattService =
+                new BluetoothGattService(UUID_GTBS, BluetoothGattService.SERVICE_TYPE_PRIMARY);
+        gattService.addCharacteristic(mBearerProviderNameCharacteristic);
+        gattService.addCharacteristic(mBearerUciCharacteristic);
+        gattService.addCharacteristic(mBearerTechnologyCharacteristic);
+        gattService.addCharacteristic(mBearerUriSchemesSupportedListCharacteristic);
+        gattService.addCharacteristic(mBearerListCurrentCallsCharacteristic);
+        gattService.addCharacteristic(mContentControlIdCharacteristic);
+        gattService.addCharacteristic(mStatusFlagsCharacteristic);
+        gattService.addCharacteristic(mCallStateCharacteristic);
+        gattService.addCharacteristic(mCallControlPointCharacteristic);
+        gattService.addCharacteristic(mCallControlPointOptionalOpcodesCharacteristic);
+        gattService.addCharacteristic(mTerminationReasonCharacteristic);
+        gattService.addCharacteristic(mIncomingCallCharacteristic);
+        gattService.addCharacteristic(mCallFriendlyNameCharacteristic);
+
+        return mBluetoothGattServer.addService(gattService);
+    }
+
+    public void cleanup() {
+        if (mBluetoothGattServer == null) {
+            return;
+        }
+        mBluetoothGattServer.close();
+        mBluetoothGattServer = null;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    /** Class that handles GATT characteristic notifications */
+    private class BluetoothGattCharacteristicNotifier {
+
+        private List<BluetoothDevice> mSubscribers = new ArrayList<>();
+
+        public int setSubscriptionConfiguration(BluetoothDevice device, byte[] configuration) {
+            if (Arrays.equals(configuration, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
+                mSubscribers.remove(device);
+            } else if (!isSubscribed(device)) {
+                mSubscribers.add(device);
+            }
+
+            return BluetoothGatt.GATT_SUCCESS;
+        }
+
+        public byte[] getSubscriptionConfiguration(BluetoothDevice device) {
+            if (isSubscribed(device)) {
+                return BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
+            }
+
+            return BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
+        }
+
+        public boolean isSubscribed(BluetoothDevice device) {
+            return mSubscribers.contains(device);
+        }
+
+        private void notifyCharacteristicChanged(BluetoothDevice device,
+                BluetoothGattCharacteristic characteristic) {
+            if (mBluetoothGattServer != null) {
+                mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
+            }
+        }
+
+        public void notify(BluetoothDevice device, BluetoothGattCharacteristic characteristic) {
+            if (isSubscribed(device)) {
+                notifyCharacteristicChanged(device, characteristic);
+            }
+        }
+
+        public void notifyAll(BluetoothGattCharacteristic characteristic) {
+            for (BluetoothDevice device : mSubscribers) {
+                notifyCharacteristicChanged(device, characteristic);
+            }
+        }
+    }
+
+    /** Wrapper class for BluetoothGattCharacteristic */
+    private class GattCharacteristic extends BluetoothGattCharacteristic {
+
+        protected BluetoothGattCharacteristicNotifier mNotifier;
+
+        public GattCharacteristic(UUID uuid, int properties, int permissions) {
+            super(uuid, properties, permissions);
+            if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
+                mNotifier = new BluetoothGattCharacteristicNotifier();
+                addDescriptor(new ClientCharacteristicConfigurationDescriptor());
+            } else {
+                mNotifier = null;
+            }
+        }
+
+        public byte[] getSubscriptionConfiguration(BluetoothDevice device) {
+            return mNotifier.getSubscriptionConfiguration(device);
+        }
+
+        public int setSubscriptionConfiguration(BluetoothDevice device, byte[] configuration) {
+            return mNotifier.setSubscriptionConfiguration(device, configuration);
+        }
+
+        private boolean isNotifiable() {
+            return mNotifier != null;
+        }
+
+        @Override
+        public boolean setValue(byte[] value) {
+            boolean success = super.setValue(value);
+            if (success && isNotifiable()) {
+                mNotifier.notifyAll(this);
+            }
+
+            return success;
+        }
+
+        @Override
+        public boolean setValue(int value, int formatType, int offset) {
+            boolean success = super.setValue(value, formatType, offset);
+            if (success && isNotifiable()) {
+                mNotifier.notifyAll(this);
+            }
+
+            return success;
+        }
+
+        @Override
+        public boolean setValue(String value) {
+            boolean success = super.setValue(value);
+            if (success && isNotifiable()) {
+                mNotifier.notifyAll(this);
+            }
+
+            return success;
+        }
+
+        public boolean clearValue(boolean notify) {
+            boolean success = super.setValue(new byte[0]);
+            if (success && notify && isNotifiable()) {
+                mNotifier.notifyAll(this);
+            }
+
+            return success;
+        }
+
+        public void handleWriteRequest(BluetoothDevice device, int requestId,
+                boolean responseNeeded, byte[] value) {
+            if (responseNeeded) {
+                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0,
+                        value);
+            }
+        }
+    }
+
+    private class CallControlPointCharacteristic extends GattCharacteristic {
+
+        public CallControlPointCharacteristic() {
+            super(UUID_CALL_CONTROL_POINT,
+                    PROPERTY_WRITE | PROPERTY_WRITE_NO_RESPONSE | PROPERTY_NOTIFY,
+                    PERMISSION_WRITE_ENCRYPTED);
+        }
+
+        @Override
+        public void handleWriteRequest(BluetoothDevice device, int requestId,
+                boolean responseNeeded, byte[] value) {
+            int status;
+            if (value.length == 0) {
+                // at least opcode is required
+                status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
+            } else {
+                status = BluetoothGatt.GATT_SUCCESS;
+            }
+
+            if (responseNeeded) {
+                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0,
+                        value);
+            }
+
+            int opcode = (int) value[0];
+            mCallback.onCallControlPointRequest(device, opcode,
+                    Arrays.copyOfRange(value, 1, value.length));
+        }
+
+        public void setResult(BluetoothDevice device, int requestedOpcode, int callIndex,
+                int requestResult) {
+            byte[] value = new byte[3];
+            value[0] = (byte) (requestedOpcode);
+            value[1] = (byte) (callIndex);
+            value[2] = (byte) (requestResult);
+
+            super.setValue(value);
+
+            // to avoid sending control point notification before write response
+            mContext.getMainThreadHandler().post(() -> mNotifier.notify(device, this));
+        }
+    }
+
+    private class ClientCharacteristicConfigurationDescriptor extends BluetoothGattDescriptor {
+
+        ClientCharacteristicConfigurationDescriptor() {
+            super(UUID_CLIENT_CHARACTERISTIC_CONFIGURATION,
+                    PERMISSION_READ | PERMISSION_WRITE_ENCRYPTED);
+        }
+
+        public byte[] getValue(BluetoothDevice device) {
+            GattCharacteristic characteristic = (GattCharacteristic) getCharacteristic();
+            byte value[] = characteristic.getSubscriptionConfiguration(device);
+            if (value == null) {
+                return BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
+            }
+
+            return value;
+        }
+
+        public int setValue(BluetoothDevice device, byte[] value) {
+            GattCharacteristic characteristic = (GattCharacteristic) getCharacteristic();
+            int properties = characteristic.getProperties();
+
+            if (value.length != 2) {
+                return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
+
+            } else if ((!Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
+                    && !Arrays.equals(value, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
+                    && !Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE))
+                    || ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0 && Arrays
+                            .equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE))
+                    || ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == 0 && Arrays
+                            .equals(value, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE))) {
+                return BluetoothGatt.GATT_FAILURE;
+            }
+
+            return characteristic.setSubscriptionConfiguration(device, value);
+        }
+    }
+
+    public boolean setBearerProviderName(String providerName) {
+        return mBearerProviderNameCharacteristic.setValue(providerName);
+    }
+
+    public boolean setBearerTechnology(int technology) {
+        return mBearerTechnologyCharacteristic.setValue(technology,
+                BluetoothGattCharacteristic.FORMAT_UINT8, 0);
+    }
+
+    public boolean setBearerUriSchemesSupportedList(List<String> bearerUriSchemesSupportedList) {
+        return mBearerUriSchemesSupportedListCharacteristic
+                .setValue(String.join(",", bearerUriSchemesSupportedList));
+    }
+
+    public boolean setCallState(Map<Integer, TbsCall> callsList) {
+        if (DBG) {
+            Log.d(TAG, "setCallState: callsList=" + callsList);
+        }
+        int i = 0;
+        byte[] value = new byte[callsList.size() * 3];
+        for (Map.Entry<Integer, TbsCall> entry : callsList.entrySet()) {
+            TbsCall call = entry.getValue();
+            value[i++] = (byte) (entry.getKey() & 0xff);
+            value[i++] = (byte) (call.getState() & 0xff);
+            value[i++] = (byte) (call.getFlags() & 0xff);
+        }
+
+        return mCallStateCharacteristic.setValue(value);
+    }
+
+    public boolean setBearerListCurrentCalls(Map<Integer, TbsCall> callsList) {
+        if (DBG) {
+            Log.d(TAG, "setBearerListCurrentCalls: callsList=" + callsList);
+        }
+        final int listItemLengthMax = Byte.MAX_VALUE;
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        for (Map.Entry<Integer, TbsCall> entry : callsList.entrySet()) {
+            TbsCall call = entry.getValue();
+            int listItemLength = Math.min(listItemLengthMax, 3 + call.getUri().getBytes().length);
+            stream.write((byte) (listItemLength & 0xff));
+            stream.write((byte) (entry.getKey() & 0xff));
+            stream.write((byte) (call.getState() & 0xff));
+            stream.write((byte) (call.getFlags() & 0xff));
+            stream.write(call.getUri().getBytes(), 0, listItemLength - 3);
+        }
+
+        return mBearerListCurrentCallsCharacteristic.setValue(stream.toByteArray());
+    }
+
+    private boolean updateStatusFlags(int flag, boolean set) {
+        Integer valueInt = mStatusFlagsCharacteristic
+                .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0);
+
+        if (((valueInt & flag) != 0) == set) {
+            return false;
+        }
+
+        valueInt ^= flag;
+
+        return mStatusFlagsCharacteristic.setValue(valueInt,
+                BluetoothGattCharacteristic.FORMAT_UINT16, 0);
+    }
+
+    public boolean setInbandRingtoneFlag() {
+        return updateStatusFlags(STATUS_FLAG_INBAND_RINGTONE_ENABLED, true);
+    }
+
+    public boolean clearInbandRingtoneFlag() {
+        return updateStatusFlags(STATUS_FLAG_INBAND_RINGTONE_ENABLED, false);
+    }
+
+    public boolean setSilentModeFlag() {
+        return updateStatusFlags(STATUS_FLAG_SILENT_MODE_ENABLED, true);
+    }
+
+    public boolean clearSilentModeFlag() {
+        return updateStatusFlags(STATUS_FLAG_SILENT_MODE_ENABLED, false);
+    }
+
+    private void setCallControlPointOptionalOpcodes(boolean isLocalHoldOpcodeSupported,
+            boolean isJoinOpcodeSupported) {
+        int valueInt = 0;
+        if (isLocalHoldOpcodeSupported) {
+            valueInt |= CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD;
+        }
+        if (isJoinOpcodeSupported) {
+            valueInt |= CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN;
+        }
+
+        byte[] value = new byte[2];
+        value[0] = (byte) (valueInt & 0xff);
+        value[1] = (byte) ((valueInt >> 8) & 0xff);
+
+        mCallControlPointOptionalOpcodesCharacteristic.setValue(value);
+    }
+
+    public boolean setTerminationReason(int callIndex, int terminationReason) {
+        if (DBG) {
+            Log.d(TAG, "setTerminationReason: callIndex=" + callIndex + " terminationReason="
+                    + terminationReason);
+        }
+        byte[] value = new byte[2];
+        value[0] = (byte) (callIndex & 0xff);
+        value[1] = (byte) (terminationReason & 0xff);
+
+        return mTerminationReasonCharacteristic.setValue(value);
+    }
+
+    public Integer getIncomingCallIndex() {
+        byte[] value = mIncomingCallCharacteristic.getValue();
+        if (value == null || value.length == 0) {
+            return null;
+        }
+
+        return (int) value[0];
+    }
+
+    public boolean setIncomingCall(int callIndex, String uri) {
+        if (DBG) {
+            Log.d(TAG, "setIncomingCall: callIndex=" + callIndex + " uri=" + uri);
+        }
+        byte[] value = new byte[uri.length() + 1];
+        value[0] = (byte) (callIndex & 0xff);
+        System.arraycopy(uri.getBytes(), 0, value, 1, uri.length());
+
+        return mIncomingCallCharacteristic.setValue(value);
+    }
+
+    public boolean clearIncomingCall() {
+        if (DBG) {
+            Log.d(TAG, "clearIncomingCall");
+        }
+        return mIncomingCallCharacteristic.clearValue(false);
+    }
+
+    public boolean setCallFriendlyName(int callIndex, String callFriendlyName) {
+        if (DBG) {
+            Log.d(TAG, "setCallFriendlyName: callIndex=" + callIndex + "callFriendlyName="
+                    + callFriendlyName);
+        }
+        byte[] value = new byte[callFriendlyName.length() + 1];
+        value[0] = (byte) (callIndex & 0xff);
+        System.arraycopy(callFriendlyName.getBytes(), 0, value, 1, callFriendlyName.length());
+
+        return mCallFriendlyNameCharacteristic.setValue(value);
+    }
+
+    public Integer getCallFriendlyNameIndex() {
+        byte[] value = mCallFriendlyNameCharacteristic.getValue();
+        if (value == null || value.length == 0) {
+            return null;
+        }
+
+        return (int) value[0];
+    }
+
+    public boolean clearFriendlyName() {
+        if (DBG) {
+            Log.d(TAG, "clearFriendlyName");
+        }
+        return mCallFriendlyNameCharacteristic.clearValue(false);
+    }
+
+    public void setCallControlPointResult(BluetoothDevice device, int requestedOpcode,
+            int callIndex, int requestResult) {
+        if (DBG) {
+            Log.d(TAG,
+                    "setCallControlPointResult: device=" + device + " requestedOpcode="
+                            + requestedOpcode + " callIndex=" + callIndex + " requesuResult="
+                            + requestResult);
+        }
+        mCallControlPointCharacteristic.setResult(device, requestedOpcode, callIndex,
+                requestResult);
+    }
+
+    private static UUID makeUuid(String uuid16) {
+        return UUID.fromString(UUID_PREFIX + uuid16 + UUID_SUFFIX);
+    }
+
+    /**
+     * Callback to handle incoming requests to the GATT server. All read/write requests for
+     * characteristics and descriptors are handled here.
+     */
+    @VisibleForTesting
+    final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
+
+        @Override
+        public void onServiceAdded(int status, BluetoothGattService service) {
+            if (DBG) {
+                Log.d(TAG, "onServiceAdded: status=" + status);
+            }
+            if (mCallback != null) {
+                mCallback.onServiceAdded(status == BluetoothGatt.GATT_SUCCESS);
+            }
+        }
+
+        @Override
+        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
+                BluetoothGattCharacteristic characteristic) {
+            if (DBG) {
+                Log.d(TAG, "onCharacteristicReadRequest: device=" + device);
+            }
+            GattCharacteristic gattCharacteristic = (GattCharacteristic) characteristic;
+            byte[] value = gattCharacteristic.getValue();
+            if (value == null) {
+                value = new byte[0];
+            }
+
+            int status;
+            if (value.length < offset) {
+                status = BluetoothGatt.GATT_INVALID_OFFSET;
+            } else {
+                value = Arrays.copyOfRange(value, offset, value.length);
+                status = BluetoothGatt.GATT_SUCCESS;
+            }
+
+            mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
+        }
+
+        @Override
+        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+                BluetoothGattCharacteristic characteristic, boolean preparedWrite,
+                boolean responseNeeded, int offset, byte[] value) {
+            if (DBG) {
+                Log.d(TAG, "onCharacteristicWriteRequest: device=" + device);
+            }
+            GattCharacteristic gattCharacteristic = (GattCharacteristic) characteristic;
+            int status;
+            if (preparedWrite) {
+                status = BluetoothGatt.GATT_FAILURE;
+            } else if (offset > 0) {
+                status = BluetoothGatt.GATT_INVALID_OFFSET;
+            } else {
+                gattCharacteristic.handleWriteRequest(device, requestId, responseNeeded, value);
+                return;
+            }
+
+            if (responseNeeded) {
+                mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
+            }
+        }
+
+        @Override
+        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
+                BluetoothGattDescriptor descriptor) {
+            if (DBG) {
+                Log.d(TAG, "onDescriptorReadRequest: device=" + device);
+            }
+            ClientCharacteristicConfigurationDescriptor cccd =
+                    (ClientCharacteristicConfigurationDescriptor) descriptor;
+            byte[] value = cccd.getValue(device);
+            int status;
+            if (value.length < offset) {
+                status = BluetoothGatt.GATT_INVALID_OFFSET;
+            } else {
+                value = Arrays.copyOfRange(value, offset, value.length);
+                status = BluetoothGatt.GATT_SUCCESS;
+            }
+
+            mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
+        }
+
+        @Override
+        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+                BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
+                int offset, byte[] value) {
+            if (DBG) {
+                Log.d(TAG, "onDescriptorWriteRequest: device=" + device);
+            }
+            ClientCharacteristicConfigurationDescriptor cccd =
+                    (ClientCharacteristicConfigurationDescriptor) descriptor;
+            int status;
+            if (preparedWrite) {
+                // TODO: handle prepareWrite
+                status = BluetoothGatt.GATT_FAILURE;
+            } else if (offset > 0) {
+                status = BluetoothGatt.GATT_INVALID_OFFSET;
+            } else if (value.length != 2) {
+                status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
+            } else {
+                status = cccd.setValue(device, value);
+            }
+
+            if (responseNeeded) {
+                mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
+            }
+        }
+    };
+}
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
new file mode 100644
index 0000000..c3a07ba
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
@@ -0,0 +1,937 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothLeCallControl;
+import android.bluetooth.BluetoothLeCall;
+import android.bluetooth.IBluetoothLeCallControlCallback;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.bluetooth.le_audio.ContentControlIdKeeper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+
+/** Container class to store TBS instances */
+public class TbsGeneric {
+
+    private static final String TAG = "TbsGeneric";
+    private static final boolean DBG = true;
+
+    private static final String UCI = "GTBS";
+    private static final String DEFAULT_PROVIDER_NAME = "none";
+    private static final int DEFAULT_BEARER_TECHNOLOGY = 0x00;
+    private static final String UNKNOWN_FRIENDLY_NAME = "unknown";
+
+    /** Class representing the pending request sent to the application */
+    private class Request {
+        BluetoothDevice device;
+        List<UUID> callIdList;
+        int requestedOpcode;
+        int callIndex;
+
+        public Request(BluetoothDevice device, UUID callId, int requestedOpcode, int callIndex) {
+            this.device = device;
+            this.callIdList = Arrays.asList(callId);
+            this.requestedOpcode = requestedOpcode;
+            this.callIndex = callIndex;
+        }
+
+        public Request(BluetoothDevice device, List<ParcelUuid> callIds, int requestedOpcode,
+                int callIndex) {
+            this.device = device;
+            this.callIdList = new ArrayList<>();
+            for (ParcelUuid callId : callIds) {
+                this.callIdList.add(callId.getUuid());
+            }
+            this.requestedOpcode = requestedOpcode;
+            this.callIndex = callIndex;
+        }
+    }
+
+    /* Application-registered TBS instance */
+    private class Bearer {
+        final String token;
+        final IBluetoothLeCallControlCallback callback;
+        final String uci;
+        List<String> uriSchemes;
+        final int capabilities;
+        final int ccid;
+        String providerName;
+        int technology;
+        Map<UUID, Integer> callIdIndexMap = new HashMap<>();
+        Map<Integer, Request> requestMap = new HashMap<>();
+
+        public Bearer(String token, IBluetoothLeCallControlCallback callback, String uci,
+                List<String> uriSchemes, int capabilities, String providerName, int technology,
+                int ccid) {
+            this.token = token;
+            this.callback = callback;
+            this.uci = uci;
+            this.uriSchemes = uriSchemes;
+            this.capabilities = capabilities;
+            this.providerName = providerName;
+            this.technology = technology;
+            this.ccid = ccid;
+        }
+    }
+
+    private TbsGatt mTbsGatt = null;
+    private List<Bearer> mBearerList = new ArrayList<>();
+    private int mLastIndexAssigned = TbsCall.INDEX_UNASSIGNED;
+    private Map<Integer, TbsCall> mCurrentCallsList = new TreeMap<>();
+    private Bearer mForegroundBearer = null;
+    private int mLastRequestIdAssigned = 0;
+    private List<String> mUriSchemes = new ArrayList<>(Arrays.asList("tel"));
+
+    public boolean init(TbsGatt tbsGatt) {
+        if (DBG) {
+            Log.d(TAG, "init");
+        }
+
+        mTbsGatt = tbsGatt;
+
+        int ccid = ContentControlIdKeeper.acquireCcid();
+        if (!isCcidValid(ccid)) {
+            return false;
+        }
+
+        return mTbsGatt.init(ccid, UCI, mUriSchemes, true, true, DEFAULT_PROVIDER_NAME,
+                DEFAULT_BEARER_TECHNOLOGY, mTbsGattCallback);
+    }
+
+    public void cleanup() {
+        if (DBG) {
+            Log.d(TAG, "cleanup");
+        }
+
+        if (mTbsGatt != null) {
+            mTbsGatt.cleanup();
+            mTbsGatt = null;
+        }
+    }
+
+    private Bearer getBearerByToken(String token) {
+        synchronized (mBearerList) {
+            for (Bearer bearer : mBearerList) {
+                if (bearer.token.equals(token)) {
+                    return bearer;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private Bearer getBearerByCcid(int ccid) {
+        synchronized (mBearerList) {
+            for (Bearer bearer : mBearerList) {
+                if (bearer.ccid == ccid) {
+                    return bearer;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private Bearer getBearerSupportingUri(String uri) {
+        synchronized (mBearerList) {
+            for (Bearer bearer : mBearerList) {
+                for (String s : bearer.uriSchemes) {
+                    if (uri.startsWith(s + ":")) {
+                        return bearer;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private Map.Entry<UUID, Bearer> getCallIdByIndex(int callIndex) {
+        synchronized (mBearerList) {
+            for (Bearer bearer : mBearerList) {
+                for (Map.Entry<UUID, Integer> callIdToIndex : bearer.callIdIndexMap.entrySet()) {
+                    if (callIndex == callIdToIndex.getValue()) {
+                        return Map.entry(callIdToIndex.getKey(), bearer);
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public boolean addBearer(String token, IBluetoothLeCallControlCallback callback, String uci,
+            List<String> uriSchemes, int capabilities, String providerName, int technology) {
+        if (DBG) {
+            Log.d(TAG,
+                    "addBearer: token=" + token + " uci=" + uci + " uriSchemes=" + uriSchemes
+                            + " capabilities=" + capabilities + " providerName=" + providerName
+                            + " technology=" + technology);
+        }
+        if (getBearerByToken(token) != null) {
+            Log.w(TAG, "addBearer: token=" + token + " registered already");
+            return false;
+        }
+
+        // Acquire CCID for TbsObject. The CCID is released on remove()
+        Bearer bearer = new Bearer(token, callback, uci, uriSchemes, capabilities, providerName,
+                technology, ContentControlIdKeeper.acquireCcid());
+        if (isCcidValid(bearer.ccid)) {
+            synchronized (mBearerList) {
+                mBearerList.add(bearer);
+            }
+
+            updateUriSchemesSupported();
+            if (mForegroundBearer == null) {
+                setForegroundBearer(bearer);
+            }
+        } else {
+            Log.e(TAG, "Failed to acquire ccid");
+        }
+
+        if (callback != null) {
+            try {
+                Log.d(TAG, "ccid=" + bearer.ccid);
+                callback.onBearerRegistered(bearer.ccid);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return isCcidValid(bearer.ccid);
+    }
+
+    public void removeBearer(String token) {
+        if (DBG) {
+            Log.d(TAG, "removeBearer: token=" + token);
+        }
+        Bearer bearer = getBearerByToken(token);
+        if (bearer == null) {
+            return;
+        }
+
+        // Remove the calls associated with this bearer
+        for (Integer callIndex : bearer.callIdIndexMap.values()) {
+            mCurrentCallsList.remove(callIndex);
+        }
+
+        if (bearer.callIdIndexMap.size() > 0) {
+            notifyCclc();
+        }
+
+        // Release the ccid acquired
+        ContentControlIdKeeper.releaseCcid(bearer.ccid);
+
+        mBearerList.remove(bearer);
+
+        updateUriSchemesSupported();
+        if (mForegroundBearer == bearer) {
+            setForegroundBearer(findNewForegroundBearer());
+        }
+    }
+
+    private void checkRequestComplete(Bearer bearer, UUID callId, TbsCall tbsCall) {
+        // check if there's any pending request related to this call
+        Map.Entry<Integer, Request> requestEntry = null;
+        if (bearer.requestMap.size() > 0) {
+            for (Map.Entry<Integer, Request> entry : bearer.requestMap.entrySet()) {
+                if (entry.getValue().callIdList.contains(callId)) {
+                    requestEntry = entry;
+                }
+            }
+        }
+
+        if (requestEntry == null) {
+            if (DBG) {
+                Log.d(TAG, "requestEntry is null");
+            }
+            return;
+        }
+
+        int requestId = requestEntry.getKey();
+        Request request = requestEntry.getValue();
+
+        int result;
+        if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE) {
+            if (mCurrentCallsList.get(request.callIndex) == null) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            } else {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+        } else if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT) {
+            if (tbsCall.getState() != BluetoothLeCall.STATE_INCOMING) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            } else {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+        } else if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD) {
+            if (tbsCall.getState() == BluetoothLeCall.STATE_LOCALLY_HELD
+                    || tbsCall.getState() == BluetoothLeCall.STATE_LOCALLY_AND_REMOTELY_HELD) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            } else {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+        } else if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE) {
+            if (tbsCall.getState() != BluetoothLeCall.STATE_LOCALLY_HELD
+                    && tbsCall.getState() != BluetoothLeCall.STATE_LOCALLY_AND_REMOTELY_HELD) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            } else {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+        } else if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE) {
+            if (bearer.callIdIndexMap.get(request.callIdList.get(0)) != null) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            } else {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+        } else if (request.requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN) {
+            /* While joining calls, those that are not in remotely held state should go to active */
+            if (bearer.callIdIndexMap.get(callId) == null
+                    || (tbsCall.getState() != BluetoothLeCall.STATE_ACTIVE
+                            && tbsCall.getState() != BluetoothLeCall.STATE_REMOTELY_HELD)) {
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            } else {
+                /* Check if all of the pending calls transit to required state */
+                for (UUID pendingCallId : request.callIdList) {
+                    Integer callIndex = bearer.callIdIndexMap.get(pendingCallId);
+                    TbsCall pendingTbsCall = mCurrentCallsList.get(callIndex);
+                    if (pendingTbsCall.getState() != BluetoothLeCall.STATE_ACTIVE
+                            && pendingTbsCall.getState() != BluetoothLeCall.STATE_REMOTELY_HELD) {
+                        /* Still waiting for more call state updates */
+                        return;
+                    }
+                }
+                result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+            }
+        } else {
+            result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+
+        }
+
+        mTbsGatt.setCallControlPointResult(request.device, request.requestedOpcode,
+                request.callIndex, result);
+
+        bearer.requestMap.remove(requestId);
+    }
+
+    private int getTbsResult(int result, int requestedOpcode) {
+        if (result == BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID) {
+            return TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+        }
+
+        if (result == BluetoothLeCallControl.RESULT_ERROR_INVALID_URI
+                && requestedOpcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE) {
+            return TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI;
+        }
+
+        return TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+    }
+
+    public void requestResult(int ccid, int requestId, int result) {
+        if (DBG) {
+            Log.d(TAG, "requestResult: ccid=" + ccid + " requestId=" + requestId + " result="
+                    + result);
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            Log.i(TAG, " Bearer for ccid " + ccid + " does not exist");
+            return;
+        }
+
+        if (result == BluetoothLeCallControl.RESULT_SUCCESS) {
+            // don't send the success here, wait for state transition instead
+            return;
+        }
+
+        // check if there's any pending request related to this call
+        Request request = bearer.requestMap.remove(requestId);
+        if (request == null) {
+            // already sent response
+            return;
+        }
+
+        int tbsResult = getTbsResult(result, request.requestedOpcode);
+        mTbsGatt.setCallControlPointResult(request.device, request.requestedOpcode,
+                request.callIndex, tbsResult);
+    }
+
+    public void callAdded(int ccid, BluetoothLeCall call) {
+        if (DBG) {
+            Log.d(TAG, "callAdded: ccid=" + ccid + " call=" + call);
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            Log.e(TAG, "callAdded: unknown ccid=" + ccid);
+            return;
+        }
+
+        UUID callId = call.getUuid();
+        if (bearer.callIdIndexMap.containsKey(callId)) {
+            Log.e(TAG, "callAdded: uuidId=" + callId + " already on list");
+            return;
+        }
+
+        Integer callIndex = getFreeCallIndex();
+        if (callIndex == null) {
+            Log.e(TAG, "callAdded: out of call indices!");
+            return;
+        }
+
+        bearer.callIdIndexMap.put(callId, callIndex);
+        TbsCall tbsCall = TbsCall.create(call);
+        mCurrentCallsList.put(callIndex, tbsCall);
+
+        checkRequestComplete(bearer, callId, tbsCall);
+        if (tbsCall.isIncoming()) {
+            mTbsGatt.setIncomingCall(callIndex, tbsCall.getUri());
+        }
+
+        String friendlyName = tbsCall.getFriendlyName();
+        if (friendlyName == null) {
+            friendlyName = UNKNOWN_FRIENDLY_NAME;
+        }
+        mTbsGatt.setCallFriendlyName(callIndex, friendlyName);
+
+        notifyCclc();
+        if (mForegroundBearer != bearer) {
+            setForegroundBearer(bearer);
+        }
+    }
+
+    public void callRemoved(int ccid, UUID callId, int reason) {
+        if (DBG) {
+            Log.d(TAG, "callRemoved: ccid=" + ccid + "reason=" + reason);
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            Log.e(TAG, "callRemoved: unknown ccid=" + ccid);
+            return;
+        }
+
+        Integer callIndex = bearer.callIdIndexMap.remove(callId);
+        TbsCall tbsCall = mCurrentCallsList.remove(callIndex);
+        if (tbsCall == null) {
+            Log.e(TAG, "callRemoved: no such call");
+            return;
+        }
+
+        checkRequestComplete(bearer, callId, tbsCall);
+        mTbsGatt.setTerminationReason(callIndex, reason);
+        notifyCclc();
+
+        Integer incomingCallIndex = mTbsGatt.getIncomingCallIndex();
+        if (incomingCallIndex != null && incomingCallIndex.equals(callIndex)) {
+            mTbsGatt.clearIncomingCall();
+            // TODO: check if there's any incoming call more???
+        }
+
+        Integer friendlyNameCallIndex = mTbsGatt.getCallFriendlyNameIndex();
+        if (friendlyNameCallIndex != null && friendlyNameCallIndex.equals(callIndex)) {
+            mTbsGatt.clearFriendlyName();
+            // TODO: check if there's any incoming/outgoing call more???
+        }
+    }
+
+    public void callStateChanged(int ccid, UUID callId, int state) {
+        if (DBG) {
+            Log.d(TAG, "callStateChanged: ccid=" + ccid + " callId=" + callId + " state=" + state);
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            Log.e(TAG, "callStateChanged: unknown ccid=" + ccid);
+            return;
+        }
+
+        Integer callIndex = bearer.callIdIndexMap.get(callId);
+        if (callIndex == null) {
+            Log.e(TAG, "callStateChanged: unknown callId=" + callId);
+            return;
+        }
+
+        TbsCall tbsCall = mCurrentCallsList.get(callIndex);
+        if (tbsCall.getState() == state) {
+            return;
+        }
+
+        tbsCall.setState(state);
+
+        checkRequestComplete(bearer, callId, tbsCall);
+        notifyCclc();
+
+        Integer incomingCallIndex = mTbsGatt.getIncomingCallIndex();
+        if (incomingCallIndex != null && incomingCallIndex.equals(callIndex)) {
+            mTbsGatt.clearIncomingCall();
+            // TODO: check if there's any incoming call more???
+        }
+    }
+
+    public void currentCallsList(int ccid, List<BluetoothLeCall> calls) {
+        if (DBG) {
+            Log.d(TAG, "currentCallsList: ccid=" + ccid + " callsNum=" + calls.size());
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            Log.e(TAG, "currentCallsList: unknown ccid=" + ccid);
+            return;
+        }
+
+        boolean cclc = false;
+        Map<UUID, Integer> storedCallIdList = new HashMap<>(bearer.callIdIndexMap);
+        bearer.callIdIndexMap = new HashMap<>();
+        for (BluetoothLeCall call : calls) {
+            UUID callId = call.getUuid();
+            Integer callIndex = storedCallIdList.get(callId);
+            if (callIndex == null) {
+                // new call
+                callIndex = getFreeCallIndex();
+                if (callIndex == null) {
+                    Log.e(TAG, "currentCallsList: out of call indices!");
+                    continue;
+                }
+
+                mCurrentCallsList.put(callIndex, TbsCall.create(call));
+                cclc |= true;
+            } else {
+                TbsCall tbsCall = mCurrentCallsList.get(callIndex);
+                TbsCall tbsCallNew = TbsCall.create(call);
+                if (tbsCall != tbsCallNew) {
+                    mCurrentCallsList.replace(callIndex, tbsCallNew);
+                    cclc |= true;
+                }
+            }
+
+            bearer.callIdIndexMap.put(callId, callIndex);
+        }
+
+        for (Map.Entry<UUID, Integer> callIdToIndex : storedCallIdList.entrySet()) {
+            if (!bearer.callIdIndexMap.containsKey(callIdToIndex.getKey())) {
+                mCurrentCallsList.remove(callIdToIndex.getValue());
+                cclc |= true;
+            }
+        }
+
+        if (cclc) {
+            notifyCclc();
+        }
+    }
+
+    public void networkStateChanged(int ccid, String providerName, int technology) {
+        if (DBG) {
+            Log.d(TAG, "networkStateChanged: ccid=" + ccid + " providerName=" + providerName
+                    + " technology=" + technology);
+        }
+        Bearer bearer = getBearerByCcid(ccid);
+        if (bearer == null) {
+            return;
+        }
+
+        boolean providerChanged = !bearer.providerName.equals(providerName);
+        if (providerChanged) {
+            bearer.providerName = providerName;
+        }
+
+        boolean technologyChanged = bearer.technology != technology;
+        if (technologyChanged) {
+            bearer.technology = technology;
+        }
+
+        if (bearer == mForegroundBearer) {
+            if (providerChanged) {
+                mTbsGatt.setBearerProviderName(bearer.providerName);
+            }
+
+            if (technologyChanged) {
+                mTbsGatt.setBearerTechnology(bearer.technology);
+            }
+        }
+    }
+
+    private int processOriginateCall(BluetoothDevice device, String uri) {
+        if (uri.startsWith("tel")) {
+            /*
+             * FIXME: For now, process telephone call originate request here, as
+             * BluetoothInCallService might be not running. The BluetoothInCallService is active
+             * when there is a call only.
+             */
+            Log.i(TAG, "originate uri=" + uri);
+            Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.parse(uri));
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mTbsGatt.getContext().startActivity(intent);
+            mTbsGatt.setCallControlPointResult(device, TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE,
+                    TbsCall.INDEX_UNASSIGNED, TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS);
+        } else {
+            UUID callId = UUID.randomUUID();
+            int requestId = mLastRequestIdAssigned + 1;
+            Request request = new Request(device, callId,
+                    TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, TbsCall.INDEX_UNASSIGNED);
+
+            Bearer bearer = getBearerSupportingUri(uri);
+            if (bearer == null) {
+                return TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI;
+            }
+
+            try {
+                bearer.callback.onPlaceCall(requestId, new ParcelUuid(callId), uri);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                return TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+            }
+
+            bearer.requestMap.put(requestId, request);
+            mLastIndexAssigned = requestId;
+        }
+
+
+        return TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+    }
+
+    private final TbsGatt.Callback mTbsGattCallback = new TbsGatt.Callback() {
+
+        @Override
+        public void onServiceAdded(boolean success) {
+            if (DBG) {
+                Log.d(TAG, "onServiceAdded: success=" + success);
+            }
+        }
+
+        @Override
+        public void onCallControlPointRequest(BluetoothDevice device, int opcode, byte[] args) {
+            if (DBG) {
+                Log.d(TAG, "onCallControlPointRequest: device=" + device + " opcode=" + opcode
+                        + "argsLen=" + args.length);
+            }
+            int result;
+
+            switch (opcode) {
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT:
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE:
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD:
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE: {
+                    if (args.length == 0) {
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+                        break;
+                    }
+
+                    int callIndex = args[0];
+                    Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
+                    if (entry == null) {
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+                        break;
+                    }
+
+                    TbsCall call = mCurrentCallsList.get(callIndex);
+                    if (!isCallStateTransitionValid(call.getState(), opcode)) {
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_STATE_MISMATCH;
+                        break;
+                    }
+
+                    Bearer bearer = entry.getValue();
+                    UUID callId = entry.getKey();
+                    int requestId = mLastRequestIdAssigned + 1;
+                    Request request = new Request(device, callId, opcode, callIndex);
+                    try {
+                        if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT) {
+                            bearer.callback.onAcceptCall(requestId, new ParcelUuid(callId));
+                        } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE) {
+                            bearer.callback.onTerminateCall(requestId, new ParcelUuid(callId));
+                        } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD) {
+                            if ((bearer.capabilities & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
+                                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+                                break;
+                            }
+                            bearer.callback.onHoldCall(requestId, new ParcelUuid(callId));
+                        } else {
+                            if ((bearer.capabilities & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
+                                result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+                                break;
+                            }
+                            bearer.callback.onUnholdCall(requestId, new ParcelUuid(callId));
+                        }
+                    } catch (RemoteException e) {
+                        e.printStackTrace();
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+                        break;
+                    }
+
+                    bearer.requestMap.put(requestId, request);
+                    mLastRequestIdAssigned = requestId;
+
+                    result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+                    break;
+                }
+
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE: {
+                    result = processOriginateCall(device, new String(args));
+                    break;
+                }
+
+                case TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN: {
+                    // at least 2 call indices are required
+                    if (args.length < 2) {
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+                        break;
+                    }
+
+                    Map.Entry<UUID, Bearer> firstEntry = null;
+                    List<ParcelUuid> parcelUuids = new ArrayList<>();
+                    for (int callIndex : args) {
+                        Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
+                        if (entry == null) {
+                            result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+                            break;
+                        }
+
+                        // state transition is valid, because a call in any state can requested to
+                        // join
+
+                        if (firstEntry == null) {
+                            firstEntry = entry;
+                        }
+
+                        if (firstEntry.getValue() != entry.getValue()) {
+                            Log.w(TAG, "Cannot join calls from different bearers!");
+                            result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+                            break;
+                        }
+
+                        parcelUuids.add(new ParcelUuid(entry.getKey()));
+                    }
+
+                    Bearer bearer = firstEntry.getValue();
+                    Request request = new Request(device, parcelUuids, opcode, args[0]);
+                    int requestId = mLastRequestIdAssigned + 1;
+                    try {
+                        bearer.callback.onJoinCalls(requestId, parcelUuids);
+                    } catch (RemoteException e) {
+                        e.printStackTrace();
+                        result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+                        break;
+                    }
+
+                    bearer.requestMap.put(requestId, request);
+                    mLastIndexAssigned = requestId;
+
+                    result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+                    break;
+                }
+
+                default:
+                    result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+                    break;
+            }
+
+            if (result == TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS) {
+                // return here and wait for the request completition from application
+                return;
+            }
+
+            mTbsGatt.setCallControlPointResult(device, opcode, 0, result);
+        }
+    };
+
+    private static boolean isCcidValid(int ccid) {
+        return ccid != ContentControlIdKeeper.CCID_INVALID;
+    }
+
+    private static boolean isCallIndexAssigned(int callIndex) {
+        return callIndex != TbsCall.INDEX_UNASSIGNED;
+    }
+
+    private Integer getFreeCallIndex() {
+        int callIndex = mLastIndexAssigned;
+        for (int i = TbsCall.INDEX_MIN; i <= TbsCall.INDEX_MAX; i++) {
+            callIndex = (callIndex + 1) % TbsCall.INDEX_MAX;
+            if (!isCallIndexAssigned(callIndex)) {
+                continue;
+            }
+
+            if (mCurrentCallsList.keySet().contains(callIndex)) {
+                continue;
+            }
+
+            mLastIndexAssigned = callIndex;
+
+            return callIndex;
+        }
+
+        return null;
+    }
+
+    private Map.Entry<Integer, TbsCall> getCallByStates(LinkedHashSet<Integer> states) {
+        for (Map.Entry<Integer, TbsCall> entry : mCurrentCallsList.entrySet()) {
+            if (states.contains(entry.getValue().getState())) {
+                return entry;
+            }
+        }
+
+        return null;
+    }
+
+    private Map.Entry<Integer, TbsCall> getForegroundCall() {
+        LinkedHashSet<Integer> states = new LinkedHashSet<Integer>();
+        Map.Entry<Integer, TbsCall> foregroundCall;
+
+        if (mCurrentCallsList.size() == 0) {
+            return null;
+        }
+
+        states.add(BluetoothLeCall.STATE_INCOMING);
+        foregroundCall = getCallByStates(states);
+        if (foregroundCall != null) {
+            return foregroundCall;
+        }
+
+        states.clear();
+        states.add(BluetoothLeCall.STATE_DIALING);
+        states.add(BluetoothLeCall.STATE_ALERTING);
+        foregroundCall = getCallByStates(states);
+        if (foregroundCall != null) {
+            return foregroundCall;
+        }
+
+        states.clear();
+        states.add(BluetoothLeCall.STATE_ACTIVE);
+        foregroundCall = getCallByStates(states);
+        if (foregroundCall != null) {
+            return foregroundCall;
+        }
+
+        return null;
+    }
+
+    private Bearer findNewForegroundBearer() {
+        if (mBearerList.size() == 0) {
+            return null;
+        }
+
+        // the bearer that owns the foreground call
+        Map.Entry<Integer, TbsCall> foregroundCall = getForegroundCall();
+        if (foregroundCall != null) {
+            for (Bearer bearer : mBearerList) {
+                if (bearer.callIdIndexMap.values().contains(foregroundCall.getKey())) {
+                    return bearer;
+                }
+            }
+        }
+
+        // the last bearer registered
+        return mBearerList.get(mBearerList.size() - 1);
+    }
+
+    private void setForegroundBearer(Bearer bearer) {
+        if (DBG) {
+            Log.d(TAG, "setForegroundBearer: bearer=" + bearer);
+        }
+
+        if (bearer == null) {
+            mTbsGatt.setBearerProviderName(DEFAULT_PROVIDER_NAME);
+            mTbsGatt.setBearerTechnology(DEFAULT_BEARER_TECHNOLOGY);
+        } else if (mForegroundBearer == null) {
+            mTbsGatt.setBearerProviderName(bearer.providerName);
+            mTbsGatt.setBearerTechnology(bearer.technology);
+        } else {
+            if (!bearer.providerName.equals(mForegroundBearer.providerName)) {
+                mTbsGatt.setBearerProviderName(bearer.providerName);
+            }
+
+            if (bearer.technology != mForegroundBearer.technology) {
+                mTbsGatt.setBearerTechnology(bearer.technology);
+            }
+        }
+
+        mForegroundBearer = bearer;
+    }
+
+    private void notifyCclc() {
+        if (DBG) {
+            Log.d(TAG, "notifyCclc");
+        }
+        mTbsGatt.setCallState(mCurrentCallsList);
+        mTbsGatt.setBearerListCurrentCalls(mCurrentCallsList);
+    }
+
+    private void updateUriSchemesSupported() {
+        List<String> newUriSchemes = new ArrayList<>();
+        for (Bearer bearer : mBearerList) {
+            newUriSchemes.addAll(bearer.uriSchemes);
+        }
+
+        // filter duplicates
+        newUriSchemes = new ArrayList<>(new HashSet<>(newUriSchemes));
+        if (newUriSchemes.equals(mUriSchemes)) {
+            return;
+        }
+
+        mUriSchemes = new ArrayList<>(newUriSchemes);
+        mTbsGatt.setBearerUriSchemesSupportedList(mUriSchemes);
+    }
+
+    private static boolean isCallStateTransitionValid(int callState, int requestedOpcode) {
+        switch (requestedOpcode) {
+            case TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT:
+                if (callState == BluetoothLeCall.STATE_INCOMING) {
+                    return true;
+                }
+                break;
+
+            case TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE:
+                // Any call can be terminated.
+                return true;
+
+            case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD:
+                if (callState == BluetoothLeCall.STATE_INCOMING
+                        || callState == BluetoothLeCall.STATE_ACTIVE
+                        || callState == BluetoothLeCall.STATE_REMOTELY_HELD) {
+                    return true;
+                }
+                break;
+
+            case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE:
+                if (callState == BluetoothLeCall.STATE_LOCALLY_HELD
+                        || callState == BluetoothLeCall.STATE_LOCALLY_AND_REMOTELY_HELD) {
+                    return true;
+                }
+                break;
+
+            default:
+                Log.e(TAG, "unhandled opcode " + requestedOpcode);
+        }
+
+        return false;
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsService.java b/android/app/src/com/android/bluetooth/tbs/TbsService.java
new file mode 100644
index 0000000..8b04ec7
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/tbs/TbsService.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import android.Manifest;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothLeCallControl;
+import android.bluetooth.BluetoothLeCall;
+import android.bluetooth.IBluetoothLeCallControl;
+import android.bluetooth.IBluetoothLeCallControlCallback;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
+
+import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+public class TbsService extends ProfileService {
+
+    private static final String TAG = "TbsService";
+    private static final boolean DBG = true;
+
+    private static TbsService sTbsService;
+
+    private final TbsGeneric mTbsGeneric = new TbsGeneric();
+
+    @Override
+    protected IProfileServiceBinder initBinder() {
+        return new TbsServerBinder(this);
+    }
+
+    @Override
+    protected void create() {
+        if (DBG) {
+            Log.d(TAG, "create()");
+        }
+    }
+
+    @Override
+    protected boolean start() {
+
+        if (DBG) {
+            Log.d(TAG, "start()");
+        }
+        if (sTbsService != null) {
+            throw new IllegalStateException("start() called twice");
+        }
+
+        // Mark service as started
+        setTbsService(this);
+
+        mTbsGeneric.init(new TbsGatt(this));
+
+        return true;
+    }
+
+    @Override
+    protected boolean stop() {
+        if (DBG) {
+            Log.d(TAG, "stop()");
+        }
+        if (sTbsService == null) {
+            Log.w(TAG, "stop() called before start()");
+            return true;
+        }
+
+        // Mark service as stopped
+        setTbsService(null);
+
+        if (mTbsGeneric != null) {
+            mTbsGeneric.cleanup();
+        }
+
+        return true;
+    }
+
+    @Override
+    protected void cleanup() {
+        if (DBG) {
+            Log.d(TAG, "cleanup()");
+        }
+    }
+
+    /**
+     * Get the TbsService instance
+     *
+     * @return TbsService instance
+     */
+    public static synchronized TbsService getTbsService() {
+        if (sTbsService == null) {
+            Log.w(TAG, "getTbsService: service is NULL");
+            return null;
+        }
+
+        if (!sTbsService.isAvailable()) {
+            Log.w(TAG, "getTbsService: service is not available");
+            return null;
+        }
+
+        return sTbsService;
+    }
+
+    private static synchronized void setTbsService(TbsService instance) {
+        if (DBG) {
+            Log.d(TAG, "setTbsService: set to=" + instance);
+        }
+
+        sTbsService = instance;
+    }
+
+    /** Binder object: must be a static class or memory leak may occur */
+    @VisibleForTesting
+    static class TbsServerBinder extends IBluetoothLeCallControl.Stub implements IProfileServiceBinder {
+        private TbsService mService;
+
+        private TbsService getService() {
+            if (!Utils.checkCallerIsSystemOrActiveUser(TAG)) {
+                Log.w(TAG, "TbsService call not allowed for non-active user");
+                return null;
+            }
+
+            if (mService != null) {
+                if (DBG) {
+                    Log.d(TAG, "Service available");
+                }
+
+                enforceBluetoothPrivilegedPermission(mService);
+                return mService;
+            }
+
+            return null;
+        }
+
+        TbsServerBinder(TbsService service) {
+            mService = service;
+        }
+
+        @Override
+        public void cleanup() {
+            mService = null;
+        }
+
+        @Override
+        public void registerBearer(String token, IBluetoothLeCallControlCallback callback, String uci,
+                List<String> uriSchemes, int capabilities, String providerName, int technology) {
+            TbsService service = getService();
+            if (service != null) {
+                service.registerBearer(token, callback, uci, uriSchemes, capabilities, providerName,
+                        technology);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void unregisterBearer(String token) {
+            TbsService service = getService();
+            if (service != null) {
+                service.unregisterBearer(token);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void requestResult(int ccid, int requestId, int result) {
+            TbsService service = getService();
+            if (service != null) {
+                service.requestResult(ccid, requestId, result);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void callAdded(int ccid, BluetoothLeCall call) {
+            TbsService service = getService();
+            if (service != null) {
+                service.callAdded(ccid, call);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void callRemoved(int ccid, ParcelUuid callId, int reason) {
+            TbsService service = getService();
+            if (service != null) {
+                service.callRemoved(ccid, callId.getUuid(), reason);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void callStateChanged(int ccid, ParcelUuid callId, int state) {
+            TbsService service = getService();
+            if (service != null) {
+                service.callStateChanged(ccid, callId.getUuid(), state);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void currentCallsList(int ccid, List<BluetoothLeCall> calls) {
+            TbsService service = getService();
+            if (service != null) {
+                service.currentCallsList(ccid, calls);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+
+        @Override
+        public void networkStateChanged(int ccid, String providerName, int technology) {
+            TbsService service = getService();
+            if (service != null) {
+                service.networkStateChanged(ccid, providerName, technology);
+            } else {
+                Log.w(TAG, "Service not active");
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void registerBearer(String token, IBluetoothLeCallControlCallback callback, String uci,
+            List<String> uriSchemes, int capabilities, String providerName, int technology) {
+        if (DBG) {
+            Log.d(TAG, "registerBearer: token=" + token);
+        }
+
+        boolean success = mTbsGeneric.addBearer(token, callback, uci, uriSchemes, capabilities,
+                providerName, technology);
+        if (success) {
+            try {
+                callback.asBinder().linkToDeath(() -> {
+                    Log.e(TAG, token + " application died, removing...");
+                    unregisterBearer(token);
+                }, 0);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (DBG) {
+            Log.d(TAG, "registerBearer: token=" + token + " success=" + success);
+        }
+    }
+
+    @VisibleForTesting
+    void unregisterBearer(String token) {
+        if (DBG) {
+            Log.d(TAG, "unregisterBearer: token=" + token);
+        }
+
+        mTbsGeneric.removeBearer(token);
+    }
+
+    @VisibleForTesting
+    public void requestResult(int ccid, int requestId, int result) {
+        if (DBG) {
+            Log.d(TAG, "requestResult: ccid=" + ccid + " requestId=" + requestId + " result="
+                    + result);
+        }
+
+        mTbsGeneric.requestResult(ccid, requestId, result);
+    }
+
+    @VisibleForTesting
+    void callAdded(int ccid, BluetoothLeCall call) {
+        if (DBG) {
+            Log.d(TAG, "callAdded: ccid=" + ccid + " call=" + call);
+        }
+
+        mTbsGeneric.callAdded(ccid, call);
+    }
+
+    @VisibleForTesting
+    void callRemoved(int ccid, UUID callId, int reason) {
+        if (DBG) {
+            Log.d(TAG, "callRemoved: ccid=" + ccid + " callId=" + callId + " reason=" + reason);
+        }
+
+        mTbsGeneric.callRemoved(ccid, callId, reason);
+    }
+
+    @VisibleForTesting
+    void callStateChanged(int ccid, UUID callId, int state) {
+        if (DBG) {
+            Log.d(TAG, "callStateChanged: ccid=" + ccid + " callId=" + callId + " state=" + state);
+        }
+
+        mTbsGeneric.callStateChanged(ccid, callId, state);
+    }
+
+    @VisibleForTesting
+    void currentCallsList(int ccid, List<BluetoothLeCall> calls) {
+        if (DBG) {
+            Log.d(TAG, "currentCallsList: ccid=" + ccid + " calls=" + calls);
+        }
+
+        mTbsGeneric.currentCallsList(ccid, calls);
+    }
+
+    @VisibleForTesting
+    void networkStateChanged(int ccid, String providerName, int technology) {
+        if (DBG) {
+            Log.d(TAG, "networkStateChanged: ccid=" + ccid + " providerName=" + providerName
+                    + " technology=" + technology);
+        }
+
+        mTbsGeneric.networkStateChanged(ccid, providerName, technology);
+    }
+
+    @Override
+    public void dump(StringBuilder sb) {
+        super.dump(sb);
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/telephony/BluetoothCall.java b/android/app/src/com/android/bluetooth/telephony/BluetoothCall.java
index 935ad25..c4c9342 100644
--- a/android/app/src/com/android/bluetooth/telephony/BluetoothCall.java
+++ b/android/app/src/com/android/bluetooth/telephony/BluetoothCall.java
@@ -16,10 +16,12 @@
 
 package com.android.bluetooth.telephony;
 
+import android.bluetooth.BluetoothLeCallControl;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.telecom.Call;
+import android.telecom.DisconnectCause;
 import android.telecom.GatewayInfo;
 import android.telecom.InCallService;
 import android.telecom.PhoneAccountHandle;
@@ -28,6 +30,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 
 /**
  * A proxy class of android.telecom.Call that
@@ -43,6 +46,7 @@
 public class BluetoothCall {
 
     private Call mCall;
+    private UUID mCallId;
 
     public Call getCall() {
         return mCall;
@@ -58,6 +62,20 @@
 
     public BluetoothCall(Call call) {
         mCall = call;
+        mCallId = UUID.randomUUID();
+    }
+
+    public BluetoothCall(Call call, UUID callId) {
+        mCall = call;
+        mCallId = callId;
+    }
+
+    public UUID getTbsCallId() {
+        return mCallId;
+    }
+
+    public void setTbsCallId(UUID callId) {
+        mCallId = callId;
     }
 
     public String getRemainingPostDialSequence() {
@@ -297,6 +315,10 @@
                 !can(Call.Details.CAPABILITY_MERGE_CONFERENCE);
     }
 
+    public DisconnectCause getDisconnectCause() {
+        return getDetails().getDisconnectCause();
+    }
+
     /**
      * Returns the list of ids of corresponding Call List.
      */
diff --git a/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java b/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java
index a9a700a..c2442fc 100644
--- a/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java
+++ b/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java
@@ -20,6 +20,8 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothLeCallControl;
+import android.bluetooth.BluetoothLeCall;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,6 +37,7 @@
 import android.telecom.Call;
 import android.telecom.CallAudioState;
 import android.telecom.Connection;
+import android.telecom.DisconnectCause;
 import android.telecom.InCallService;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -49,14 +52,21 @@
 
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.hfp.BluetoothHeadsetProxy;
+import com.android.bluetooth.tbs.BluetoothLeCallControlProxy;
 
+import java.util.Arrays;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
@@ -107,6 +117,8 @@
 
     private static final Object LOCK = new Object();
     private BluetoothHeadsetProxy mBluetoothHeadset;
+    private BluetoothLeCallControlProxy mBluetoothLeCallControl;
+    private ExecutorService mExecutor;
 
     private Semaphore mDisconnectionToneSemaphore = new Semaphore(0);
     private int mAudioMode = AudioManager.MODE_INVALID;
@@ -144,15 +156,24 @@
                 @Override
                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
                     synchronized (LOCK) {
-                        setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
-                        updateHeadsetWithCallState(true /* force */);
+                        if (profile == BluetoothProfile.HEADSET) {
+                            setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
+                            updateHeadsetWithCallState(true /* force */);
+                        } else {
+                           setBluetoothLeCallControl(new BluetoothLeCallControlProxy((BluetoothLeCallControl) proxy));
+                           sendTbsCurrentCallsList();
+                        }
                     }
                 }
 
                 @Override
                 public void onServiceDisconnected(int profile) {
                     synchronized (LOCK) {
-                        setBluetoothHeadset(null);
+                        if (profile == BluetoothProfile.HEADSET) {
+                            setBluetoothHeadset(null);
+                        } else {
+                            setBluetoothLeCallControl(null);
+                        }
                     }
                 }
             };
@@ -209,6 +230,11 @@
                 return;
             }
 
+            Integer tbsCallState = getTbsCallState(call);
+            if (mBluetoothLeCallControl != null && tbsCallState != null) {
+                mBluetoothLeCallControl.onCallStateChanged(call.getTbsCallId(), tbsCallState);
+            }
+
             // If a BluetoothCall is being put on hold because of a new connecting call, ignore the
             // CONNECTING since the BT state update needs to send out the numHeld = 1 + dialing
             // state atomically.
@@ -355,6 +381,7 @@
     public BluetoothInCallService() {
         Log.i(TAG, "BluetoothInCallService is created");
         sInstance = this;
+        mExecutor = Executors.newSingleThreadExecutor();
     }
 
     public static BluetoothInCallService getInstance() {
@@ -511,6 +538,11 @@
 
             mBluetoothCallHashMap.put(call.getId(), call);
             updateHeadsetWithCallState(false /* force */);
+
+            BluetoothLeCall tbsCall = createTbsCall(call);
+            if (mBluetoothLeCallControl != null && tbsCall != null) {
+                mBluetoothLeCallControl.onCallAdded(tbsCall);
+            }
         }
     }
 
@@ -584,6 +616,10 @@
 
         mClccIndexMap.remove(getClccMapKey(call));
         updateHeadsetWithCallState(false /* force */);
+
+        if (mBluetoothLeCallControl != null) {
+            mBluetoothLeCallControl.onCallRemoved(call.getTbsCallId(), getTbsTerminationReason(call));
+        }
     }
 
     @Override
@@ -610,6 +646,8 @@
         super.onCreate();
         BluetoothAdapter.getDefaultAdapter()
                 .getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET);
+        BluetoothAdapter.getDefaultAdapter().
+                getProfileProxy(this, mProfileListener, BluetoothProfile.LE_CALL_CONTROL);
         mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
         IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
         registerReceiver(mBluetoothAdapterReceiver, intentFilter);
@@ -640,6 +678,9 @@
             mBluetoothHeadset.closeBluetoothHeadsetProxy(this);
             mBluetoothHeadset = null;
         }
+        if (mBluetoothLeCallControl != null) {
+            mBluetoothLeCallControl.unregisterBearer();
+        }
         mProfileListener = null;
         sInstance = null;
         mCallbacks.clear();
@@ -1250,5 +1291,238 @@
         public boolean isNullCall(BluetoothCall call) {
             return call == null || call.isCallNull();
         }
+
+        public BluetoothCall getCallByCallId(UUID callId) {
+            List<BluetoothCall> calls = getBluetoothCalls();
+            for (BluetoothCall call : calls) {
+                Log.i(TAG, "getCallByCallId lookingFor=" + callId + " has=" + call.getTbsCallId());
+                if (callId.equals(call.getTbsCallId())) {
+                    return call;
+                }
+            }
+            return null;
+        }
+    };
+
+    @VisibleForTesting
+    public void setBluetoothLeCallControl(BluetoothLeCallControlProxy bluetoothTbs) {
+        mBluetoothLeCallControl = bluetoothTbs;
+
+        if ((mBluetoothLeCallControl) != null && (mTelecomManager != null)) {
+            mBluetoothLeCallControl.registerBearer(TAG, new ArrayList<String>(Arrays.asList("tel")),
+                    BluetoothLeCallControl.CAPABILITY_HOLD_CALL, getNetworkOperator(), 0x01, mExecutor,
+                    mBluetoothLeCallControlCallback);
+        }
+    }
+
+    private Integer getTbsCallState(BluetoothCall call) {
+        switch (call.getState()) {
+        case Call.STATE_ACTIVE:
+            return BluetoothLeCall.STATE_ACTIVE;
+
+        case Call.STATE_CONNECTING:
+        case Call.STATE_SELECT_PHONE_ACCOUNT:
+            return BluetoothLeCall.STATE_DIALING;
+
+        case Call.STATE_DIALING:
+        case Call.STATE_PULLING_CALL:
+            return BluetoothLeCall.STATE_ALERTING;
+
+        case Call.STATE_HOLDING:
+            return BluetoothLeCall.STATE_LOCALLY_HELD;
+
+        case Call.STATE_RINGING:
+        case Call.STATE_SIMULATED_RINGING:
+            if (call.isSilentRingingRequested()) {
+                return null;
+            } else {
+                return BluetoothLeCall.STATE_INCOMING;
+            }
+        }
+        return null;
+    }
+
+    private int getTbsTerminationReason(BluetoothCall call) {
+        DisconnectCause cause = call.getDisconnectCause();
+        if (cause == null) {
+            Log.w(TAG, " termination cause is null");
+            return BluetoothLeCallControl.TERMINATION_REASON_FAIL;
+        }
+
+        switch (cause.getCode()) {
+        case DisconnectCause.BUSY:
+            return BluetoothLeCallControl.TERMINATION_REASON_LINE_BUSY;
+        case DisconnectCause.REMOTE:
+        case DisconnectCause.REJECTED:
+            return BluetoothLeCallControl.TERMINATION_REASON_REMOTE_HANGUP;
+        case DisconnectCause.LOCAL:
+            return BluetoothLeCallControl.TERMINATION_REASON_SERVER_HANGUP;
+        case DisconnectCause.ERROR:
+            return BluetoothLeCallControl.TERMINATION_REASON_NETWORK_CONGESTION;
+        case DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED:
+            return BluetoothLeCallControl.TERMINATION_REASON_INVALID_URI;
+        default:
+            return BluetoothLeCallControl.TERMINATION_REASON_FAIL;
+        }
+    }
+
+    private BluetoothLeCall createTbsCall(BluetoothCall call) {
+        Integer state = getTbsCallState(call);
+        boolean isPartOfConference = false;
+        boolean isConferenceWithNoChildren = call.isConference()
+                && call.can(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+
+        if (state == null) {
+            return null;
+        }
+
+        BluetoothCall conferenceCall = getBluetoothCallById(call.getParentId());
+        if (!mCallInfo.isNullCall(conferenceCall)) {
+            isPartOfConference = true;
+
+            // Run some alternative states for Conference-level merge/swap support.
+            // Basically, if BluetoothCall supports swapping or merging at the
+            // conference-level,
+            // then we need to expose the calls as having distinct states
+            // (ACTIVE vs CAPABILITY_HOLD) or
+            // the functionality won't show up on the bluetooth device.
+
+            // Before doing any special logic, ensure that we are dealing with an
+            // ACTIVE BluetoothCall and that the conference itself has a notion of
+            // the current "active" child call.
+            BluetoothCall activeChild =
+                getBluetoothCallById(conferenceCall.getGenericConferenceActiveChildCallId());
+            if (state == BluetoothLeCall.STATE_ACTIVE && !mCallInfo.isNullCall(activeChild)) {
+                // Reevaluate state if we can MERGE or if we can SWAP without previously having
+                // MERGED.
+                boolean shouldReevaluateState =
+                        conferenceCall.can(Connection.CAPABILITY_MERGE_CONFERENCE)
+                                    || (conferenceCall.can(Connection.CAPABILITY_SWAP_CONFERENCE)
+                                    && !conferenceCall.wasConferencePreviouslyMerged());
+
+                if (shouldReevaluateState) {
+                    isPartOfConference = false;
+                    if (call == activeChild) {
+                        state = BluetoothLeCall.STATE_ACTIVE;
+                    } else {
+                        // At this point we know there is an "active" child and we know that it is
+                        // not this call, so set it to HELD instead.
+                        state = BluetoothLeCall.STATE_LOCALLY_HELD;
+                    }
+                }
+            }
+            if (conferenceCall.getState() == Call.STATE_HOLDING
+                    && conferenceCall.can(Connection.CAPABILITY_MANAGE_CONFERENCE)) {
+                // If the parent IMS CEP conference BluetoothCall is on hold, we should mark
+                // this BluetoothCall as being on hold regardless of what the other
+                // children are doing.
+                state = BluetoothLeCall.STATE_LOCALLY_HELD;
+            }
+        } else if (isConferenceWithNoChildren) {
+            // Handle the special case of an IMS conference BluetoothCall without conference
+            // event package support.
+            // The BluetoothCall will be marked as a conference, but the conference will not
+            // have
+            // child calls where conference event packages are not used by the carrier.
+            isPartOfConference = true;
+        }
+
+        final Uri addressUri;
+        if (call.getGatewayInfo() != null) {
+            addressUri = call.getGatewayInfo().getOriginalAddress();
+        } else {
+            addressUri = call.getHandle();
+        }
+
+        String uri = addressUri == null ? null : addressUri.toString();
+        int callFlags = call.isIncoming() ? 0 : BluetoothLeCall.FLAG_OUTGOING_CALL;
+
+        return new BluetoothLeCall(call.getTbsCallId(), uri, call.getCallerDisplayName(), state,
+                       callFlags);
+    }
+
+    private void sendTbsCurrentCallsList() {
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+
+        for (BluetoothCall call : mBluetoothCallHashMap.values()) {
+            BluetoothLeCall tbsCall = createTbsCall(call);
+            if (tbsCall != null) {
+                tbsCalls.add(tbsCall);
+            }
+        }
+
+        mBluetoothLeCallControl.currentCallsList(tbsCalls);
+    }
+
+    private final BluetoothLeCallControl.Callback mBluetoothLeCallControlCallback = new BluetoothLeCallControl.Callback() {
+
+        @Override
+        public void onAcceptCall(int requestId, UUID callId) {
+            synchronized (LOCK) {
+                enforceModifyPermission();
+                Log.i(TAG, "TBS - accept call=" + callId);
+                int result = BluetoothLeCallControl.RESULT_SUCCESS;
+                BluetoothCall call = mCallInfo.getCallByCallId(callId);
+                if (mCallInfo.isNullCall(call)) {
+                    result = BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID;
+                } else {
+                    call.answer(VideoProfile.STATE_AUDIO_ONLY);
+                }
+                mBluetoothLeCallControl.requestResult(requestId, result);
+            }
+        }
+
+        @Override
+        public void onTerminateCall(int requestId, UUID callId) {
+            synchronized (LOCK) {
+                enforceModifyPermission();
+                Log.i(TAG, "TBS - terminate call=" + callId);
+                int result = BluetoothLeCallControl.RESULT_SUCCESS;
+                BluetoothCall call = mCallInfo.getCallByCallId(callId);
+                if (mCallInfo.isNullCall(call)) {
+                    result = BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID;
+                } else {
+                    call.disconnect();
+                }
+                mBluetoothLeCallControl.requestResult(requestId, result);
+            }
+        }
+
+        @Override
+        public void onHoldCall(int requestId, UUID callId) {
+            synchronized (LOCK) {
+                enforceModifyPermission();
+                Log.i(TAG, "TBS - hold call=" + callId);
+                int result = BluetoothLeCallControl.RESULT_SUCCESS;
+                BluetoothCall call = mCallInfo.getCallByCallId(callId);
+                if (mCallInfo.isNullCall(call)) {
+                    result = BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID;
+                } else {
+                    call.hold();
+                }
+                mBluetoothLeCallControl.requestResult(requestId, result);
+            }
+        }
+
+        @Override
+        public void onUnholdCall(int requestId, UUID callId) {
+            synchronized (LOCK) {
+                enforceModifyPermission();
+                Log.i(TAG, "TBS - unhold call=" + callId);
+                int result = BluetoothLeCallControl.RESULT_SUCCESS;
+                BluetoothCall call = mCallInfo.getCallByCallId(callId);
+                if (mCallInfo.isNullCall(call)) {
+                    result = BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID;
+                } else {
+                    call.unhold();
+                }
+                mBluetoothLeCallControl.requestResult(requestId, result);
+            }
+        }
+
+        @Override
+        public void onPlaceCall(int requestId, UUID callId, String uri) {
+            mBluetoothLeCallControl.requestResult(requestId, BluetoothLeCallControl.RESULT_ERROR_APPLICATION);
+        }
     };
 };
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index c0132aa..bc6483b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -1082,6 +1082,29 @@
         }
     }
 
+    @Test
+    public void testDatabaseMigration_108_109() throws IOException {
+        String testString = "TEST STRING";
+        // Create a database with version 108
+        SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 108);
+        // insert a device to the database
+        ContentValues device = new ContentValues();
+        device.put("address", TEST_BT_ADDR);
+        device.put("migrated", false);
+        assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+                CoreMatchers.not(-1));
+        // Migrate database from 108 to 109
+        db.close();
+        db = testHelper.runMigrationsAndValidate(DB_NAME, 109, true,
+                MetadataDatabase.MIGRATION_108_109);
+        Cursor cursor = db.query("SELECT * FROM metadata");
+        assertHasColumn(cursor, "le_call_control_connection_policy", true);
+        while (cursor.moveToNext()) {
+            // Check the new columns was added with default value
+            assertColumnIntData(cursor, "le_call_control_connection_policy", 100);
+        }
+    }
+
     /**
      * Helper function to check whether the database has the expected column
      */
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/109.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/109.json
new file mode 100644
index 0000000..52c0aed
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/109.json
@@ -0,0 +1,304 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 109,
+    "identityHash": "9431bd51ea289b1b6236ba4aa4ce6bcf",
+    "entities": [
+      {
+        "tableName": "metadata",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, PRIMARY KEY(`address`))",
+        "fields": [
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "migrated",
+            "columnName": "migrated",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpSupportsOptionalCodecs",
+            "columnName": "a2dpSupportsOptionalCodecs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "a2dpOptionalCodecsEnabled",
+            "columnName": "a2dpOptionalCodecsEnabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "last_active_time",
+            "columnName": "last_active_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "is_active_a2dp_device",
+            "columnName": "is_active_a2dp_device",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+            "columnName": "a2dp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+            "columnName": "a2dp_sink_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+            "columnName": "hfp_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+            "columnName": "hfp_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+            "columnName": "hid_host_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+            "columnName": "pan_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+            "columnName": "pbap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+            "columnName": "pbap_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_connection_policy",
+            "columnName": "map_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+            "columnName": "sap_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+            "columnName": "hearing_aid_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+            "columnName": "map_client_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+            "columnName": "le_audio_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+            "columnName": "volume_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+            "columnName": "csip_set_coordinator_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+            "columnName": "le_call_control_connection_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.manufacturer_name",
+            "columnName": "manufacturer_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.model_name",
+            "columnName": "model_name",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.software_version",
+            "columnName": "software_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.hardware_version",
+            "columnName": "hardware_version",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.companion_app",
+            "columnName": "companion_app",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_icon",
+            "columnName": "main_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.is_untethered_headset",
+            "columnName": "is_untethered_headset",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_icon",
+            "columnName": "untethered_left_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_icon",
+            "columnName": "untethered_right_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_icon",
+            "columnName": "untethered_case_icon",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_battery",
+            "columnName": "untethered_left_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_battery",
+            "columnName": "untethered_right_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_battery",
+            "columnName": "untethered_case_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_charging",
+            "columnName": "untethered_left_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_charging",
+            "columnName": "untethered_right_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_charging",
+            "columnName": "untethered_case_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+            "columnName": "enhanced_settings_ui_uri",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.device_type",
+            "columnName": "device_type",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_battery",
+            "columnName": "main_battery",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_charging",
+            "columnName": "main_charging",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.main_low_battery_threshold",
+            "columnName": "main_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+            "columnName": "untethered_left_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+            "columnName": "untethered_right_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+            "columnName": "untethered_case_low_battery_threshold",
+            "affinity": "BLOB",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "address"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9431bd51ea289b1b6236ba4aa4ce6bcf')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
new file mode 100644
index 0000000..f2181a1
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2021 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 com.android.bluetooth.tbs;
+
+import static org.mockito.Mockito.*;
+import static org.mockito.AdditionalMatchers.*;
+
+import android.bluetooth.*;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothLeCallControl;
+import android.bluetooth.IBluetoothLeCallControlCallback;
+import android.content.Context;
+import android.os.Looper;
+import android.util.Pair;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.internal.R;
+import com.google.common.primitives.Bytes;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.UUID;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TbsGattTest {
+    private static Context sContext;
+
+    private BluetoothAdapter mAdapter;
+    private BluetoothDevice mCurrentDevice;
+
+    private Integer mCurrentCcid;
+    private String mCurrentUci;
+    private List<String> mCurrentUriSchemes;
+    private int mCurrentFeatureFlags;
+    private String mCurrentProviderName;
+    private int mCurrentTechnology;
+
+    private TbsGatt mTbsGatt;
+
+    @Mock
+    private AdapterService mAdapterService;
+    @Mock
+    private BluetoothGattServerProxy mMockGattServer;
+    @Mock
+    private TbsGatt.Callback mMockTbsGattCallback;
+
+    @Rule
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+    @Captor
+    private ArgumentCaptor<BluetoothGattService> mGattServiceCaptor;
+
+    @BeforeClass
+    public static void setUpOnce() {
+        sContext = getInstrumentation().getTargetContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        MockitoAnnotations.initMocks(this);
+
+        TestUtils.setAdapterService(mAdapterService);
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+        doReturn(true).when(mMockGattServer).addService(any(BluetoothGattService.class));
+        doReturn(true).when(mMockGattServer).open(any(BluetoothGattServerCallback.class));
+
+        mTbsGatt = new TbsGatt(sContext);
+        mTbsGatt.setBluetoothGattServerForTesting(mMockGattServer);
+
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mCurrentDevice = null;
+        mTbsGatt = null;
+        TestUtils.clearAdapterService(mAdapterService);
+    }
+
+    private void prepareDefaultService() {
+        mCurrentCcid = 122;
+        mCurrentUci = "un" + mCurrentCcid.toString();
+        mCurrentUriSchemes = new ArrayList<String>(Arrays.asList("tel"));
+        mCurrentProviderName = "unknown";
+        mCurrentTechnology = 0x00;
+
+        Assert.assertTrue(mTbsGatt.init(mCurrentCcid, mCurrentUci, mCurrentUriSchemes, true, true,
+                mCurrentProviderName, mCurrentTechnology, mMockTbsGattCallback));
+        Assert.assertNotNull(mMockGattServer);
+
+        verify(mMockGattServer).addService(mGattServiceCaptor.capture());
+        Assert.assertNotNull(mMockGattServer);
+    }
+
+    private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+        BluetoothGattService service = mGattServiceCaptor.getValue();
+        BluetoothGattCharacteristic characteristic = service.getCharacteristic(uuid);
+        Assert.assertNotNull(characteristic);
+
+        return characteristic;
+    }
+
+    private void configureNotifications(BluetoothDevice device,
+            BluetoothGattCharacteristic characteristic, boolean enable) {
+        BluetoothGattDescriptor descriptor =
+                characteristic.getDescriptor(TbsGatt.UUID_CLIENT_CHARACTERISTIC_CONFIGURATION);
+        Assert.assertNotNull(descriptor);
+
+        mTbsGatt.mGattServerCallback.onDescriptorWriteRequest(device, 1, descriptor, false, true, 0,
+                enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
+                        : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+        verify(mMockGattServer).sendResponse(eq(mCurrentDevice), eq(1),
+                eq(BluetoothGatt.GATT_SUCCESS), eq(0), any());
+        reset(mMockGattServer);
+    }
+
+    private void verifySetValue(BluetoothGattCharacteristic characteristic, Object value,
+            boolean shouldNotify) {
+        if (characteristic.getUuid().equals(TbsGatt.UUID_BEARER_PROVIDER_NAME)) {
+            boolean valueChanged = !characteristic.getStringValue(0).equals((String) value);
+            if (valueChanged) {
+                Assert.assertTrue(mTbsGatt.setBearerProviderName((String) value));
+            } else {
+                Assert.assertFalse(mTbsGatt.setBearerProviderName((String) value));
+            }
+            Assert.assertEquals((String) value, characteristic.getStringValue(0));
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_BEARER_TECHNOLOGY)) {
+            boolean valueChanged = characteristic
+                    .getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0) != (Integer) value;
+            if (valueChanged) {
+                Assert.assertTrue(mTbsGatt.setBearerTechnology((Integer) value));
+            } else {
+                Assert.assertFalse(mTbsGatt.setBearerTechnology((Integer) value));
+            }
+            Assert.assertEquals((Integer) value,
+                    characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0));
+
+        } else if (characteristic.getUuid()
+                .equals(TbsGatt.UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST)) {
+            String valueString = String.join(",", (List<String>) value);
+            boolean valueChanged = !characteristic.getStringValue(0).equals(valueString);
+            if (valueChanged) {
+                Assert.assertTrue(mTbsGatt.setBearerUriSchemesSupportedList((List<String>) value));
+            } else {
+                Assert.assertFalse(mTbsGatt.setBearerUriSchemesSupportedList((List<String>) value));
+            }
+            Assert.assertEquals(valueString, characteristic.getStringValue(0));
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_STATUS_FLAGS)) {
+
+            Pair<Integer, Boolean> flagStatePair = (Pair<Integer, Boolean>) value;
+            switch (flagStatePair.first) {
+                case TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED:
+                    if (flagStatePair.second) {
+                        Assert.assertTrue(mTbsGatt.setInbandRingtoneFlag());
+                    } else {
+                        Assert.assertTrue(mTbsGatt.clearInbandRingtoneFlag());
+                    }
+                    break;
+
+                case TbsGatt.STATUS_FLAG_SILENT_MODE_ENABLED:
+                    if (flagStatePair.second) {
+                        Assert.assertTrue(mTbsGatt.setSilentModeFlag());
+                    } else {
+                        Assert.assertTrue(mTbsGatt.clearSilentModeFlag());
+                    }
+                    break;
+
+                default:
+                    Assert.assertTrue(false);
+            }
+
+            if (flagStatePair.second) {
+                Assert.assertTrue(
+                        (characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0)
+                                & flagStatePair.first) != 0);
+            } else {
+                Assert.assertTrue(
+                        (characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0)
+                                & flagStatePair.first) == 0);
+            }
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_CALL_STATE)) {
+            Pair<Map<Integer, TbsCall>, byte[]> callsExpectedPacketPair =
+                    (Pair<Map<Integer, TbsCall>, byte[]>) value;
+            Assert.assertTrue(mTbsGatt.setCallState(callsExpectedPacketPair.first));
+            Assert.assertTrue(
+                    Arrays.equals(callsExpectedPacketPair.second, characteristic.getValue()));
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_BEARER_LIST_CURRENT_CALLS)) {
+            Pair<Map<Integer, TbsCall>, byte[]> callsExpectedPacketPair =
+                    (Pair<Map<Integer, TbsCall>, byte[]>) value;
+            Assert.assertTrue(mTbsGatt.setBearerListCurrentCalls(callsExpectedPacketPair.first));
+            Assert.assertTrue(
+                    Arrays.equals(callsExpectedPacketPair.second, characteristic.getValue()));
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_TERMINATION_REASON)) {
+            Pair<Integer, Integer> indexReasonPair = (Pair<Integer, Integer>) value;
+            Assert.assertTrue(
+                    mTbsGatt.setTerminationReason(indexReasonPair.first, indexReasonPair.second));
+            Assert.assertTrue(
+                    Arrays.equals(
+                            new byte[] {(byte) indexReasonPair.first.byteValue(),
+                                    indexReasonPair.second.byteValue()},
+                            characteristic.getValue()));
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_INCOMING_CALL)) {
+            if (value == null) {
+                Assert.assertTrue(mTbsGatt.clearIncomingCall());
+                Assert.assertEquals(0, characteristic.getValue().length);
+            } else {
+                Pair<Integer, String> indexStrPair = (Pair<Integer, String>) value;
+                Assert.assertTrue(
+                        mTbsGatt.setIncomingCall(indexStrPair.first, indexStrPair.second));
+                Assert.assertTrue(Arrays.equals(
+                        Bytes.concat(new byte[] {(byte) indexStrPair.first.byteValue()},
+                                indexStrPair.second.getBytes(StandardCharsets.UTF_8)),
+                        characteristic.getValue()));
+            }
+
+        } else if (characteristic.getUuid().equals(TbsGatt.UUID_CALL_FRIENDLY_NAME)) {
+            if (value == null) {
+                Assert.assertTrue(mTbsGatt.clearFriendlyName());
+                Assert.assertEquals(0, characteristic.getValue().length);
+            } else {
+                Pair<Integer, String> indexNamePair = (Pair<Integer, String>) value;
+                Assert.assertTrue(
+                        mTbsGatt.setCallFriendlyName(indexNamePair.first, indexNamePair.second));
+                Assert.assertTrue(Arrays.equals(
+                        Bytes.concat(new byte[] {(byte) indexNamePair.first.byteValue()},
+                                indexNamePair.second.getBytes(StandardCharsets.UTF_8)),
+                        characteristic.getValue()));
+            }
+        }
+
+        if (shouldNotify) {
+            verify(mMockGattServer).notifyCharacteristicChanged(eq(mCurrentDevice),
+                    eq(characteristic), eq(false));
+        } else {
+            verify(mMockGattServer, times(0)).notifyCharacteristicChanged(any(), any(),
+                    anyBoolean());
+        }
+
+        reset(mMockGattServer);
+    }
+
+    @Test
+    public void testSetBearerProviderName() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_BEARER_PROVIDER_NAME);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, "providerName2", true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, "providerName3", false);
+    }
+
+    @Test
+    public void testSetBearerTechnology() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, 0x04, true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, 0x05, false);
+    }
+
+    @Test
+    public void testSetUriSchemes() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, new ArrayList<>(Arrays.asList("uri2", "uri3")), true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, new ArrayList<>(Arrays.asList("uri4", "uri5")), false);
+    }
+
+    @Test
+    public void testSetCurrentCallList() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_BEARER_LIST_CURRENT_CALLS);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        Map<Integer, TbsCall> callsMap = new TreeMap<>();
+        callsMap.put(0x0A, TbsCall.create(
+                new BluetoothLeCall(UUID.randomUUID(), "tel:123456789", "John Doe", 0x03, 0x00)));
+        byte[] packetExpected = new byte[] {
+                // First call
+                (byte) 0x10, // Length of this entry (incl. URI length, excl. this length field
+                             // byte)
+                0x0A, // Call index
+                0x03, // Active call state
+                0x00, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+                0x74, 0x65, 0x6c, 0x3a, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
+                // URI: tel:123456789
+        };
+        verifySetValue(characteristic,
+                new Pair<Map<Integer, TbsCall>, byte[]>(callsMap, packetExpected), true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        callsMap.put(0x0B, TbsCall.create(new BluetoothLeCall(UUID.randomUUID(), "tel:987654321",
+                "Kate", 0x01, BluetoothLeCall.FLAG_OUTGOING_CALL)));
+        packetExpected = new byte[] {
+                // First call
+                (byte) 0x10, // Length of this entry (incl. URI length, excl. this length field
+                             // byte)
+                0x0A, // Call index
+                0x03, // Active call state
+                0x00, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+                0x74, 0x65, 0x6c, 0x3a, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
+                // URI: tel:123456789
+                // Second call
+                (byte) 0x10, // Length of this entry (incl. URI length, excl. this length field
+                             // byte)
+                0x0B, // Call index
+                0x01, // Dialing call state
+                0x01, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+                0x74, 0x65, 0x6c, 0x3a, 0x39, 0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31,
+                // URI: tel:987654321
+        };
+        verifySetValue(characteristic,
+                new Pair<Map<Integer, TbsCall>, byte[]>(callsMap, packetExpected), false);
+    }
+
+    @Test
+    public void testSetStatusFlags() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic = getCharacteristic(TbsGatt.UUID_STATUS_FLAGS);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic,
+                new Pair<Integer, Boolean>(TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED, true),
+                true);
+        verifySetValue(characteristic,
+                new Pair<Integer, Boolean>(TbsGatt.STATUS_FLAG_SILENT_MODE_ENABLED, true), true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic,
+                new Pair<Integer, Boolean>(TbsGatt.STATUS_FLAG_SILENT_MODE_ENABLED, false), false);
+    }
+
+    @Test
+    public void testSetCallState() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic = getCharacteristic(TbsGatt.UUID_CALL_STATE);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        byte[] packetExpected = new byte[] {(byte) 0x0A, // Call index
+                0x03, // Active call state
+                0x00, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+        };
+        Map<Integer, TbsCall> callsMap = new TreeMap<>();
+        callsMap.put(0x0A, TbsCall.create(
+                new BluetoothLeCall(UUID.randomUUID(), "tel:123456789", "John Doe", 0x03, 0x00)));
+        verifySetValue(characteristic,
+                new Pair<Map<Integer, TbsCall>, byte[]>(callsMap, packetExpected), true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        packetExpected = new byte[] {(byte) 0x0A, // Call index
+                0x03, // Active call state
+                0x00, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+                (byte) 0x0B, // Call index
+                0x04, // Locally Held call state
+                0x00, // Bit0:0-incoming,1-outgoing | Bit1:0-not-withheld,1-withheld |
+                      // Bit2:0-provided-by-netw.,1-withheld-by-netw.
+        };
+        callsMap.put(0x0B, TbsCall.create(
+                new BluetoothLeCall(UUID.randomUUID(), "tel:987654321", "Kate", 0x04, 0x00)));
+        verifySetValue(characteristic,
+                new Pair<Map<Integer, TbsCall>, byte[]>(callsMap, packetExpected), false);
+    }
+
+    @Test
+    public void testSetCallControlPointResult() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT);
+
+        int requestedOpcode = TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT;
+        int callIndex = 0x01;
+        int result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        mTbsGatt.setCallControlPointResult(mCurrentDevice, requestedOpcode, callIndex, result);
+        Assert.assertTrue(Arrays.equals(characteristic.getValue(),
+                new byte[] {(byte) (requestedOpcode & 0xff), (byte) (callIndex & 0xff),
+                        (byte) (result & 0xff)}));
+        verify(mMockGattServer).notifyCharacteristicChanged(eq(mCurrentDevice), eq(characteristic),
+                eq(false));
+        reset(mMockGattServer);
+
+        callIndex = 0x02;
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        mTbsGatt.setCallControlPointResult(mCurrentDevice, requestedOpcode, callIndex, result);
+        Assert.assertTrue(Arrays.equals(characteristic.getValue(),
+                new byte[] {(byte) (requestedOpcode & 0xff), (byte) (callIndex & 0xff),
+                        (byte) (result & 0xff)}));
+        verify(mMockGattServer, times(0)).notifyCharacteristicChanged(any(), any(), anyBoolean());
+    }
+
+    @Test
+    public void testSetTerminationReason() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_TERMINATION_REASON);
+
+        // Check with no CCC configured
+        verifySetValue(characteristic, new Pair<Integer, Integer>(0x0A, 0x01), false);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, new Pair<Integer, Integer>(0x0B, 0x02), true);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, new Pair<Integer, Integer>(0x0C, 0x02), false);
+    }
+
+    @Test
+    public void testSetIncomingCall() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic = getCharacteristic(TbsGatt.UUID_INCOMING_CALL);
+
+        // Check with no CCC configured
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0A, "tel:123456789"), false);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0A, "tel:987654321"), true);
+
+        // No incoming call (should not send any notification)
+        verifySetValue(characteristic, null, false);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0A, "tel:123456789"), false);
+    }
+
+    @Test
+    public void testSetFriendlyName() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_CALL_FRIENDLY_NAME);
+
+        // Check with no CCC configured
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0A, "PersonA"), false);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0B, "PersonB"), true);
+
+        // Clear freindly name (should not send any notification)
+        verifySetValue(characteristic, null, false);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        verifySetValue(characteristic, new Pair<Integer, String>(0x0C, "PersonC"), false);
+    }
+
+    @Test
+    public void testHandleControlPointRequest() {
+        prepareDefaultService();
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT);
+
+        // Call the internal GATT callback as if peer device accepts the call
+        byte[] value = new byte[] {0x00, /* opcode */ 0x0A, /* argument */ };
+        mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest(mCurrentDevice, 1, characteristic,
+                false, false, 0, value);
+
+        // Verify the higher layer callback call
+        verify(mMockTbsGattCallback).onCallControlPointRequest(eq(mCurrentDevice), eq(0x00),
+                aryEq(new byte[] {0x0A}));
+    }
+
+    @Test
+    public void testClientCharacteristicConfiguration() {
+        prepareDefaultService();
+
+        BluetoothGattCharacteristic characteristic =
+                getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY);
+        BluetoothGattDescriptor descriptor =
+                characteristic.getDescriptor(TbsGatt.UUID_CLIENT_CHARACTERISTIC_CONFIGURATION);
+
+        // Check with no configuration
+        mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mCurrentDevice, 1, 0, descriptor);
+        verify(mMockGattServer).sendResponse(eq(mCurrentDevice), eq(1),
+                eq(BluetoothGatt.GATT_SUCCESS), eq(0),
+                eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
+        reset(mMockGattServer);
+
+        // Check with notifications enabled
+        configureNotifications(mCurrentDevice, characteristic, true);
+        mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mCurrentDevice, 1, 0, descriptor);
+        verify(mMockGattServer).sendResponse(eq(mCurrentDevice), eq(1),
+                eq(BluetoothGatt.GATT_SUCCESS), eq(0),
+                eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
+        reset(mMockGattServer);
+
+        // Check with notifications disabled
+        configureNotifications(mCurrentDevice, characteristic, false);
+        mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mCurrentDevice, 1, 0, descriptor);
+        verify(mMockGattServer).sendResponse(eq(mCurrentDevice), eq(1),
+                eq(BluetoothGatt.GATT_SUCCESS), eq(0),
+                eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
+    }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
new file mode 100644
index 0000000..5d3b0ab
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.dk. 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 com.android.bluetooth.tbs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.*;
+import static org.mockito.AdditionalMatchers.*;
+
+import android.bluetooth.*;
+import android.bluetooth.IBluetoothLeCallControlCallback;
+import android.content.Context;
+import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.internal.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TbsGenericTest {
+    private BluetoothAdapter mAdapter;
+    private BluetoothDevice mCurrentDevice;
+
+    private TbsGeneric mTbsGeneric;
+
+    private @Mock TbsGatt mTbsGatt;
+    private @Mock IBluetoothLeCallControlCallback mIBluetoothLeCallControlCallback;
+    private @Captor ArgumentCaptor<Integer> mGtbsCcidCaptor;
+    private @Captor ArgumentCaptor<String> mGtbsUciCaptor;
+    private @Captor ArgumentCaptor<List> mDefaultGtbsUriSchemesCaptor =
+            ArgumentCaptor.forClass(List.class);
+    private @Captor ArgumentCaptor<String> mDefaultGtbsProviderNameCaptor;
+    private @Captor ArgumentCaptor<Integer> mDefaultGtbsTechnologyCaptor;
+
+    private @Captor ArgumentCaptor<TbsGatt.Callback> mTbsGattCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+        // Default TbsGatt mock behavior
+        doReturn(true).when(mTbsGatt).init(mGtbsCcidCaptor.capture(), mGtbsUciCaptor.capture(),
+                mDefaultGtbsUriSchemesCaptor.capture(), anyBoolean(), anyBoolean(),
+                mDefaultGtbsProviderNameCaptor.capture(), mDefaultGtbsTechnologyCaptor.capture(),
+                mTbsGattCallback.capture());
+        doReturn(true).when(mTbsGatt).setBearerProviderName(anyString());
+        doReturn(true).when(mTbsGatt).setBearerTechnology(anyInt());
+        doReturn(true).when(mTbsGatt).setBearerUriSchemesSupportedList(any());
+        doReturn(true).when(mTbsGatt).setCallState(any());
+        doReturn(true).when(mTbsGatt).setBearerListCurrentCalls(any());
+        doReturn(true).when(mTbsGatt).setInbandRingtoneFlag();
+        doReturn(true).when(mTbsGatt).clearInbandRingtoneFlag();
+        doReturn(true).when(mTbsGatt).setSilentModeFlag();
+        doReturn(true).when(mTbsGatt).clearSilentModeFlag();
+        doReturn(true).when(mTbsGatt).setTerminationReason(anyInt(), anyInt());
+        doReturn(true).when(mTbsGatt).setIncomingCall(anyInt(), anyString());
+        doReturn(true).when(mTbsGatt).clearIncomingCall();
+        doReturn(true).when(mTbsGatt).setCallFriendlyName(anyInt(), anyString());
+        doReturn(true).when(mTbsGatt).clearFriendlyName();
+
+        mTbsGeneric = new TbsGeneric();
+        mTbsGeneric.init(mTbsGatt);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTbsGeneric = null;
+    }
+
+    private Integer prepareTestBearer() {
+        String uci = "testUci";
+        List<String> uriSchemes = Arrays.asList("tel", "xmpp");
+        Integer capabilities =
+                BluetoothLeCallControl.CAPABILITY_HOLD_CALL | BluetoothLeCallControl.CAPABILITY_JOIN_CALLS;
+        String providerName = "testProviderName";
+        int technology = 0x02;
+
+        assertThat(mTbsGeneric.addBearer("testBearer", mIBluetoothLeCallControlCallback, uci, uriSchemes,
+                capabilities, providerName, technology)).isTrue();
+
+        ArgumentCaptor<Integer> ccidCaptor = ArgumentCaptor.forClass(Integer.class);
+        try {
+            // Check proper callback call on the profile's binder
+            verify(mIBluetoothLeCallControlCallback).onBearerRegistered(ccidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return ccidCaptor.getValue();
+    }
+
+    @Test
+    public void testAddBearer() {
+        prepareTestBearer();
+
+        verify(mTbsGatt).setBearerProviderName(eq("testProviderName"));
+        verify(mTbsGatt).setBearerTechnology(eq(0x02));
+
+        ArgumentCaptor<List> uriSchemesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mTbsGatt).setBearerUriSchemesSupportedList(uriSchemesCaptor.capture());
+        List<String> capturedUriSchemes = uriSchemesCaptor.getValue();
+        assertThat(capturedUriSchemes.contains("tel")).isTrue();
+        assertThat(capturedUriSchemes.contains("xmpp")).isTrue();
+    }
+
+    @Test
+    public void testRemoveBearer() {
+        prepareTestBearer();
+        reset(mTbsGatt);
+
+        mTbsGeneric.removeBearer("testBearer");
+
+        verify(mTbsGatt).setBearerProviderName(not(eq("testProviderName")));
+        verify(mTbsGatt).setBearerTechnology(not(eq(0x02)));
+
+        ArgumentCaptor<List> uriSchemesCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mTbsGatt).setBearerUriSchemesSupportedList(uriSchemesCaptor.capture());
+        List<String> capturedUriSchemes = uriSchemesCaptor.getValue();
+        assertThat(capturedUriSchemes.contains("tel")).isFalse();
+        assertThat(capturedUriSchemes.contains("xmpp")).isFalse();
+    }
+
+    @Test
+    public void testCallAdded() {
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        BluetoothLeCall tbsCall = new BluetoothLeCall(UUID.randomUUID(), "tel:987654321",
+                "aFriendlyCaller", BluetoothLeCall.STATE_INCOMING, 0);
+        mTbsGeneric.callAdded(ccid, tbsCall);
+
+        ArgumentCaptor<Integer> callIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mTbsGatt).setIncomingCall(callIndexCaptor.capture(), eq("tel:987654321"));
+        Integer capturedCallIndex = callIndexCaptor.getValue();
+        verify(mTbsGatt).setCallFriendlyName(eq(capturedCallIndex), eq("aFriendlyCaller"));
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        TbsCall capturedTbsCall = capturedCurrentCalls.get(capturedCallIndex);
+        assertThat(capturedTbsCall).isNotNull();
+        assertThat(capturedTbsCall.getState()).isEqualTo(BluetoothLeCall.STATE_INCOMING);
+        assertThat(capturedTbsCall.getUri()).isEqualTo("tel:987654321");
+        assertThat(capturedTbsCall.getFlags()).isEqualTo(0);
+        assertThat(capturedTbsCall.isIncoming()).isTrue();
+        assertThat(capturedTbsCall.getFriendlyName()).isEqualTo("aFriendlyCaller");
+    }
+
+    @Test
+    public void testCallRemoved() {
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        UUID callUuid = UUID.randomUUID();
+        BluetoothLeCall tbsCall = new BluetoothLeCall(callUuid, "tel:987654321",
+                "aFriendlyCaller", BluetoothLeCall.STATE_INCOMING, 0);
+
+        mTbsGeneric.callAdded(ccid, tbsCall);
+        ArgumentCaptor<Integer> callIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mTbsGatt).setIncomingCall(callIndexCaptor.capture(), eq("tel:987654321"));
+        Integer capturedCallIndex = callIndexCaptor.getValue();
+        reset(mTbsGatt);
+
+        doReturn(capturedCallIndex).when(mTbsGatt).getCallFriendlyNameIndex();
+        doReturn(capturedCallIndex).when(mTbsGatt).getIncomingCallIndex();
+
+        mTbsGeneric.callRemoved(ccid, callUuid, 0x01);
+        verify(mTbsGatt).clearIncomingCall();
+        verify(mTbsGatt).clearFriendlyName();
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(0);
+        verify(mTbsGatt).setBearerListCurrentCalls(currentCallsCaptor.capture());
+        assertThat(capturedCurrentCalls.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCallStateChanged() {
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        UUID callUuid = UUID.randomUUID();
+        BluetoothLeCall tbsCall = new BluetoothLeCall(callUuid, "tel:987654321",
+                "aFriendlyCaller", BluetoothLeCall.STATE_INCOMING, 0);
+
+        mTbsGeneric.callAdded(ccid, tbsCall);
+        ArgumentCaptor<Integer> callIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mTbsGatt).setIncomingCall(callIndexCaptor.capture(), eq("tel:987654321"));
+        Integer capturedCallIndex = callIndexCaptor.getValue();
+        reset(mTbsGatt);
+
+        mTbsGeneric.callStateChanged(ccid, callUuid, BluetoothLeCall.STATE_ACTIVE);
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        verify(mTbsGatt).setBearerListCurrentCalls(currentCallsCaptor.capture());
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        TbsCall capturedTbsCall = capturedCurrentCalls.get(capturedCallIndex);
+        assertThat(capturedTbsCall).isNotNull();
+        assertThat(capturedTbsCall.getState()).isEqualTo(BluetoothLeCall.STATE_ACTIVE);
+    }
+
+    @Test
+    public void testNetworkStateChanged() {
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        mTbsGeneric.networkStateChanged(ccid, "changed provider name", 0x01);
+        verify(mTbsGatt).setBearerProviderName(eq("changed provider name"));
+        verify(mTbsGatt).setBearerTechnology(eq(0x01));
+    }
+
+    @Test
+    public void testCurrentCallsList() {
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(UUID.randomUUID(), "tel:987654321", "anIncomingCaller",
+                BluetoothLeCall.STATE_INCOMING, 0));
+        tbsCalls.add(new BluetoothLeCall(UUID.randomUUID(), "tel:123456789", "anOutgoingCaller",
+                BluetoothLeCall.STATE_ALERTING, BluetoothLeCall.FLAG_OUTGOING_CALL));
+
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(2);
+        verify(mTbsGatt).setBearerListCurrentCalls(currentCallsCaptor.capture());
+        assertThat(capturedCurrentCalls.size()).isEqualTo(2);
+    }
+
+    @Test
+    public void testCallAccept() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Prepare the incoming call
+        UUID callUuid = UUID.randomUUID();
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(callUuid, "tel:987654321", "aFriendlyCaller",
+                BluetoothLeCall.STATE_INCOMING, 0));
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey();
+        reset(mTbsGatt);
+
+        byte args[] = new byte[1];
+        args[0] = (byte) (callIndex & 0xFF);
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args);
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onAcceptCall(requestIdCaptor.capture(),
+                    callUuidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid);
+
+        // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callStateChanged(ccid, callUuid, BluetoothLeCall.STATE_ACTIVE);
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT), eq(callIndex),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testCallTerminate() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Prepare the incoming call
+        UUID callUuid = UUID.randomUUID();
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(callUuid, "tel:987654321", "aFriendlyCaller",
+                BluetoothLeCall.STATE_ACTIVE, 0));
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey();
+        reset(mTbsGatt);
+
+        byte args[] = new byte[1];
+        args[0] = (byte) (callIndex & 0xFF);
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args);
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onTerminateCall(requestIdCaptor.capture(),
+                    callUuidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid);
+
+        // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callRemoved(ccid, callUuid, 0x01);
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE), eq(callIndex),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testCallHold() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Prepare the incoming call
+        UUID callUuid = UUID.randomUUID();
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(callUuid, "tel:987654321", "aFriendlyCaller",
+                BluetoothLeCall.STATE_ACTIVE, 0));
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey();
+        reset(mTbsGatt);
+
+        byte args[] = new byte[1];
+        args[0] = (byte) (callIndex & 0xFF);
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD, args);
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onHoldCall(requestIdCaptor.capture(),
+                    callUuidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid);
+
+        // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callStateChanged(ccid, callUuid, BluetoothLeCall.STATE_LOCALLY_HELD);
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD), eq(callIndex),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testCallRetrieve() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Prepare the incoming call
+        UUID callUuid = UUID.randomUUID();
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(callUuid, "tel:987654321", "aFriendlyCaller",
+                BluetoothLeCall.STATE_LOCALLY_HELD, 0));
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+        Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey();
+        reset(mTbsGatt);
+
+        byte args[] = new byte[1];
+        args[0] = (byte) (callIndex & 0xFF);
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE, args);
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onUnholdCall(requestIdCaptor.capture(),
+                    callUuidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid);
+
+        // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callStateChanged(ccid, callUuid, BluetoothLeCall.STATE_ACTIVE);
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE), eq(callIndex),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testCallOriginate() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Act as if peer originates a call via Gtbs
+        String uri = "xmpp:123456789";
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, uri.getBytes());
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onPlaceCall(requestIdCaptor.capture(),
+                    callUuidCaptor.capture(), eq(uri));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callAdded(ccid,
+                new BluetoothLeCall(callUuidCaptor.getValue().getUuid(), uri, "anOutgoingCaller",
+                        BluetoothLeCall.STATE_ALERTING, BluetoothLeCall.FLAG_OUTGOING_CALL));
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE), anyInt(),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testCallJoin() {
+        mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+        Integer ccid = prepareTestBearer();
+        reset(mTbsGatt);
+
+        // Prepare the incoming call
+        List<UUID> callUuids = Arrays.asList(UUID.randomUUID(), UUID.randomUUID());
+        List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+        tbsCalls.add(new BluetoothLeCall(callUuids.get(0), "tel:987654321", "aFriendlyCaller",
+                BluetoothLeCall.STATE_LOCALLY_HELD, 0));
+        tbsCalls.add(new BluetoothLeCall(callUuids.get(1), "tel:123456789", "a2ndFriendlyCaller",
+                BluetoothLeCall.STATE_ACTIVE, 0));
+        mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+        ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+        Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+        assertThat(capturedCurrentCalls.size()).isEqualTo(2);
+        reset(mTbsGatt);
+
+        byte args[] = new byte[capturedCurrentCalls.size()];
+        int i = 0;
+        for (Integer callIndex : capturedCurrentCalls.keySet()) {
+            args[i++] = (byte) (callIndex & 0xFF);
+        }
+        mTbsGattCallback.getValue().onCallControlPointRequest(mCurrentDevice,
+                TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN, args);
+
+        ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<List<ParcelUuid>> callUuidCaptor = ArgumentCaptor.forClass(List.class);
+        try {
+            verify(mIBluetoothLeCallControlCallback).onJoinCalls(requestIdCaptor.capture(),
+                    callUuidCaptor.capture());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        List<ParcelUuid> callParcelUuids = callUuidCaptor.getValue();
+        assertThat(callParcelUuids.size()).isEqualTo(2);
+        for (ParcelUuid callParcelUuid : callParcelUuids) {
+            assertThat(callUuids.contains(callParcelUuid.getUuid())).isEqualTo(true);
+        }
+
+        // // Respond with requestComplete...
+        mTbsGeneric.requestResult(ccid, requestIdCaptor.getValue(), BluetoothLeCallControl.RESULT_SUCCESS);
+        mTbsGeneric.callStateChanged(ccid, callUuids.get(0), BluetoothLeCall.STATE_ACTIVE);
+
+        // ..and verify if GTBS control point is updated to notifier the peer about the result
+        verify(mTbsGatt).setCallControlPointResult(eq(mCurrentDevice),
+                eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN), anyInt(),
+                eq(BluetoothLeCallControl.RESULT_SUCCESS));
+    }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
index e8c8d31..87ee7e3 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
@@ -44,6 +44,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.bluetooth.hfp.BluetoothHeadsetProxy;
+import com.android.bluetooth.tbs.BluetoothLeCallControlProxy;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -94,6 +95,7 @@
             = ServiceTestRule.withTimeout(1, TimeUnit.SECONDS);
 
     @Mock private BluetoothHeadsetProxy mMockBluetoothHeadset;
+    @Mock private BluetoothLeCallControlProxy mMockBluetoothLeCallControl;
     @Mock private BluetoothInCallService.CallInfo mMockCallInfo;
     @Mock private TelephonyManager mMockTelephonyManager;
 
@@ -136,6 +138,7 @@
 
         mBluetoothInCallService = new TestableBluetoothInCallService();
         mBluetoothInCallService.setBluetoothHeadset(mMockBluetoothHeadset);
+        mBluetoothInCallService.setBluetoothLeCallControl(mMockBluetoothLeCallControl);
         mBluetoothInCallService.mCallInfo = mMockCallInfo;
     }
 
diff --git a/system/binder/Android.bp b/system/binder/Android.bp
index 22edb39..1391206 100644
--- a/system/binder/Android.bp
+++ b/system/binder/Android.bp
@@ -46,6 +46,8 @@
         "android/bluetooth/IBluetoothMetadataListener.aidl",
         "android/bluetooth/IBluetoothGattServerCallback.aidl",
         "android/bluetooth/IBluetoothOobDataCallback.aidl",
+        "android/bluetooth/IBluetoothLeCallControl.aidl",
+        "android/bluetooth/IBluetoothLeCallControlCallback.aidl",
         "android/bluetooth/le/IAdvertisingSetCallback.aidl",
         "android/bluetooth/le/IPeriodicAdvertisingCallback.aidl",
         "android/bluetooth/le/IScannerCallback.aidl",
diff --git a/system/binder/android/bluetooth/BluetoothLeCall.aidl b/system/binder/android/bluetooth/BluetoothLeCall.aidl
new file mode 100644
index 0000000..f8d589e
--- /dev/null
+++ b/system/binder/android/bluetooth/BluetoothLeCall.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 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;
+/** @hide */
+parcelable BluetoothLeCall;
diff --git a/system/binder/android/bluetooth/IBluetoothLeCallControl.aidl b/system/binder/android/bluetooth/IBluetoothLeCallControl.aidl
new file mode 100644
index 0000000..4232fab
--- /dev/null
+++ b/system/binder/android/bluetooth/IBluetoothLeCallControl.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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.bluetooth.BluetoothLeCall;
+import android.bluetooth.IBluetoothLeCallControlCallback;
+
+import android.os.ParcelUuid;
+
+/**
+ * @hide
+ */
+oneway interface IBluetoothLeCallControl {
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void registerBearer(in String token, in IBluetoothLeCallControlCallback callback, in String uci, in List<String> uriSchemes, in int capabilities, in String provider, in int technology);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void unregisterBearer(in String token);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void requestResult(in int ccid, in int requestId, in int result);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void callAdded(in int ccid, in BluetoothLeCall call);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void callRemoved(in int ccid, in ParcelUuid callId, in int reason);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void callStateChanged(in int ccid, in ParcelUuid callId, in int state);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void currentCallsList(in int ccid, in List<BluetoothLeCall> calls);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void networkStateChanged(in int ccid, in String provider, in int technology);
+}
diff --git a/system/binder/android/bluetooth/IBluetoothLeCallControlCallback.aidl b/system/binder/android/bluetooth/IBluetoothLeCallControlCallback.aidl
new file mode 100644
index 0000000..715fa76
--- /dev/null
+++ b/system/binder/android/bluetooth/IBluetoothLeCallControlCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 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.bluetooth.BluetoothLeCall;
+
+import android.os.ParcelUuid;
+
+/**
+ * Method definitions for interacting Telephone Bearer Service instance
+ * @hide
+ */
+oneway interface IBluetoothLeCallControlCallback {
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onBearerRegistered(in int ccid);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onAcceptCall(in int requestId, in ParcelUuid uuid);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onTerminateCall(in int requestId, in ParcelUuid uuid);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onHoldCall(in int requestId, in ParcelUuid uuid);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onUnholdCall(in int requestId, in ParcelUuid uuid);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onPlaceCall(in int requestId, in ParcelUuid uuid, in String uri);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+    void onJoinCalls(in int requestId, in List<ParcelUuid> uuids);
+}