Merge "Remove mapsapi/ and refactor related files" into main
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterServiceBinder.java b/android/app/src/com/android/bluetooth/btservice/AdapterServiceBinder.java
index 9a9a91d..c080924 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterServiceBinder.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterServiceBinder.java
@@ -276,12 +276,10 @@
             return false;
         }
 
-        if (Flags.emptyNamesAreInvalid()) {
-            requireNonNull(name);
-            name = name.trim();
-            if (name.isEmpty()) {
-                throw new IllegalArgumentException("Empty names are not valid");
-            }
+        requireNonNull(name);
+        name = name.trim();
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("Empty names are not valid");
         }
 
         Log.d(TAG, "AdapterServiceBinder.setName(" + name + ")");
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
index a3789a5..48112e0 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -29,8 +29,6 @@
 import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
 import static android.bluetooth.IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME;
 
-import static com.android.bluetooth.flags.Flags.vcpDeviceVolumeApiImprovements;
-
 import static java.util.Objects.requireNonNull;
 import static java.util.Objects.requireNonNullElseGet;
 
@@ -550,12 +548,9 @@
             mDeviceVolumeCache.put(device, volume);
             mNativeInterface.setVolume(device, volume);
 
-            if (vcpDeviceVolumeApiImprovements()) {
-                // We only receive the volume change and mute state needs to be acquired manually
-                Boolean isStreamMute =
-                        mAudioManager.isStreamMute(getBluetoothContextualVolumeStream());
-                adjustDeviceMute(device, volume, isStreamMute);
-            }
+            // We only receive the volume change and mute state needs to be acquired manually
+            Boolean isStreamMute = mAudioManager.isStreamMute(getBluetoothContextualVolumeStream());
+            adjustDeviceMute(device, volume, isStreamMute);
         }
     }
 
@@ -597,10 +592,8 @@
 
         synchronized (mDeviceVolumeCache) {
             mGroupVolumeCache.put(groupId, volume);
-            if (vcpDeviceVolumeApiImprovements()) {
-                for (BluetoothDevice dev : getGroupDevices(groupId)) {
-                    mDeviceVolumeCache.put(dev, volume);
-                }
+            for (BluetoothDevice dev : getGroupDevices(groupId)) {
+                mDeviceVolumeCache.put(dev, volume);
             }
         }
         mNativeInterface.setGroupVolume(groupId, volume);
@@ -634,7 +627,7 @@
                 Log.i(TAG, "Unmute the group " + groupId);
                 unmuteGroup(groupId);
             }
-        } else if (vcpDeviceVolumeApiImprovements()) {
+        } else {
             for (BluetoothDevice device : getGroupDevices(groupId)) {
                 adjustDeviceMute(device, volume, isStreamMute);
             }
@@ -648,24 +641,20 @@
      * @return the cached volume
      */
     public int getGroupVolume(int groupId) {
-        if (vcpDeviceVolumeApiImprovements()) {
-            synchronized (mDeviceVolumeCache) {
-                Integer volume = mGroupVolumeCache.get(groupId);
+        synchronized (mDeviceVolumeCache) {
+            Integer volume = mGroupVolumeCache.get(groupId);
+            if (volume != null) {
+                return volume;
+            }
+            Log.d(TAG, "No group volume available");
+            for (BluetoothDevice device : getGroupDevices(groupId)) {
+                volume = mDeviceVolumeCache.get(device);
                 if (volume != null) {
+                    Log.w(TAG, "Volume taken from device: " + device);
                     return volume;
                 }
-                Log.d(TAG, "No group volume available");
-                for (BluetoothDevice device : getGroupDevices(groupId)) {
-                    volume = mDeviceVolumeCache.get(device);
-                    if (volume != null) {
-                        Log.w(TAG, "Volume taken from device: " + device);
-                        return volume;
-                    }
-                }
-                return VOLUME_CONTROL_UNKNOWN_VOLUME;
             }
-        } else {
-            return mGroupVolumeCache.getOrDefault(groupId, VOLUME_CONTROL_UNKNOWN_VOLUME);
+            return VOLUME_CONTROL_UNKNOWN_VOLUME;
         }
     }
 
@@ -676,17 +665,13 @@
      * @return the cached volume
      */
     public int getDeviceVolume(BluetoothDevice device) {
-        if (vcpDeviceVolumeApiImprovements()) {
-            synchronized (mDeviceVolumeCache) {
-                Integer volume = mDeviceVolumeCache.get(device);
-                if (volume != null) {
-                    return volume;
-                }
-                return mGroupVolumeCache.getOrDefault(
-                        getGroupId(device), VOLUME_CONTROL_UNKNOWN_VOLUME);
+        synchronized (mDeviceVolumeCache) {
+            Integer volume = mDeviceVolumeCache.get(device);
+            if (volume != null) {
+                return volume;
             }
-        } else {
-            return mDeviceVolumeCache.getOrDefault(device, VOLUME_CONTROL_UNKNOWN_VOLUME);
+            return mGroupVolumeCache.getOrDefault(
+                    getGroupId(device), VOLUME_CONTROL_UNKNOWN_VOLUME);
         }
     }
 
@@ -736,22 +721,18 @@
      * @return mute status
      */
     public Boolean getGroupMute(int groupId) {
-        if (vcpDeviceVolumeApiImprovements()) {
-            synchronized (mDeviceMuteCache) {
-                Boolean isMute = mGroupMuteCache.get(groupId);
+        synchronized (mDeviceMuteCache) {
+            Boolean isMute = mGroupMuteCache.get(groupId);
+            if (isMute != null) {
+                return isMute;
+            }
+            for (BluetoothDevice device : getGroupDevices(groupId)) {
+                isMute = mDeviceMuteCache.get(device);
                 if (isMute != null) {
                     return isMute;
                 }
-                for (BluetoothDevice device : getGroupDevices(groupId)) {
-                    isMute = mDeviceMuteCache.get(device);
-                    if (isMute != null) {
-                        return isMute;
-                    }
-                }
-                return false;
             }
-        } else {
-            return mGroupMuteCache.getOrDefault(groupId, false);
+            return false;
         }
     }
 
@@ -864,32 +845,16 @@
         }
 
         // If group volume has already changed, the new group member should set it
-        if (vcpDeviceVolumeApiImprovements()) {
-            int volume = getDeviceVolume(device);
-            if (volume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                Log.i(TAG, "Setting device/group volume:" + volume + " to the device:" + device);
-                setDeviceVolume(device, volume, false);
-                Boolean isDeviceMuted = getMute(device);
-                Log.i(TAG, "Setting mute:" + isDeviceMuted + " to " + device);
-                if (isDeviceMuted) {
-                    mute(device);
-                } else {
-                    unmute(device);
-                }
-            }
-        } else {
-            Integer groupVolume = getGroupVolume(groupId);
-            if (groupVolume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                Log.i(TAG, "Setting value:" + groupVolume + " to " + device);
-                mNativeInterface.setVolume(device, groupVolume);
-            }
-
-            Boolean isGroupMuted = getGroupMute(groupId);
-            Log.i(TAG, "Setting mute:" + isGroupMuted + " to " + device);
-            if (isGroupMuted) {
-                mNativeInterface.mute(device);
+        int volume = getDeviceVolume(device);
+        if (volume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
+            Log.i(TAG, "Setting device/group volume:" + volume + " to the device:" + device);
+            setDeviceVolume(device, volume, false);
+            Boolean isDeviceMuted = getMute(device);
+            Log.i(TAG, "Setting mute:" + isDeviceMuted + " to " + device);
+            if (isDeviceMuted) {
+                mute(device);
             } else {
-                mNativeInterface.unmute(device);
+                unmute(device);
             }
         }
     }
@@ -914,11 +879,9 @@
         synchronized (mDeviceVolumeCache) {
             mGroupVolumeCache.put(groupId, volume);
             mGroupMuteCache.put(groupId, mute);
-            if (vcpDeviceVolumeApiImprovements()) {
-                for (BluetoothDevice dev : getGroupDevices(groupId)) {
-                    mDeviceVolumeCache.put(dev, volume);
-                    mDeviceMuteCache.put(dev, mute);
-                }
+            for (BluetoothDevice dev : getGroupDevices(groupId)) {
+                mDeviceVolumeCache.put(dev, volume);
+                mDeviceMuteCache.put(dev, mute);
             }
         }
 
@@ -1004,56 +967,38 @@
             if (((flags & VOLUME_FLAGS_PERSISTED_USER_SET_VOLUME_MASK) == 0x01)
                     && (getConnectedDevices(groupId).size() == 1)) {
                 Log.i(TAG, "Setting device: " + device + " volume: " + volume + " to the system");
-                if (vcpDeviceVolumeApiImprovements()) {
-                    // Ignore volume from AF because persisted volume was used
-                    setIgnoreSetVolumeFromAfFlag(true);
-                }
+                // Ignore volume from AF because persisted volume was used
+                setIgnoreSetVolumeFromAfFlag(true);
                 updateGroupCacheAndAudioSystem(groupId, volume, mute, false);
                 return;
             }
 
             // Reset flag is used
-            if (vcpDeviceVolumeApiImprovements()) {
-                int deviceVolume = getDeviceVolume(device);
-                if (deviceVolume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                    Log.i(
-                            TAG,
-                            "Setting device/group volume: "
-                                    + deviceVolume
-                                    + " to the device: "
-                                    + device);
-                    setDeviceVolume(device, deviceVolume, false);
-                    Boolean isDeviceMuted = getMute(device);
-                    Log.i(TAG, "Setting mute:" + isDeviceMuted + " to " + device);
-                    if (isDeviceMuted) {
-                        mute(device);
-                    } else {
-                        unmute(device);
-                    }
-                    if (getConnectedDevices(groupId).size() == 1) {
-                        // Ignore volume from AF because cached volume was used
-                        setIgnoreSetVolumeFromAfFlag(true);
-                    }
+            int deviceVolume = getDeviceVolume(device);
+            if (deviceVolume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
+                Log.i(
+                        TAG,
+                        "Setting device/group volume: "
+                                + deviceVolume
+                                + " to the device: "
+                                + device);
+                setDeviceVolume(device, deviceVolume, false);
+                Boolean isDeviceMuted = getMute(device);
+                Log.i(TAG, "Setting mute:" + isDeviceMuted + " to " + device);
+                if (isDeviceMuted) {
+                    mute(device);
                 } else {
-                    Log.i(TAG, "Waiting for volume from AF to set to the device: " + device);
-                    if (getConnectedDevices(groupId).size() == 1) {
-                        // Clear ignore flag as volume from AF is needed
-                        setIgnoreSetVolumeFromAfFlag(false);
-                    }
+                    unmute(device);
+                }
+                if (getConnectedDevices(groupId).size() == 1) {
+                    // Ignore volume from AF because cached volume was used
+                    setIgnoreSetVolumeFromAfFlag(true);
                 }
             } else {
-                int groupVolume = getGroupVolume(groupId);
-                if (groupVolume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                    Log.i(
-                            TAG,
-                            "Setting group volume: " + groupVolume + " to the device: " + device);
-                    setGroupVolume(groupId, groupVolume);
-                } else {
-                    int systemVolume = getBleVolumeFromCurrentStream();
-                    Log.i(
-                            TAG,
-                            "Setting system volume: " + systemVolume + " to the group: " + groupId);
-                    setGroupVolume(groupId, systemVolume);
+                Log.i(TAG, "Waiting for volume from AF to set to the device: " + device);
+                if (getConnectedDevices(groupId).size() == 1) {
+                    // Clear ignore flag as volume from AF is needed
+                    setIgnoreSetVolumeFromAfFlag(false);
                 }
             }
 
@@ -1079,53 +1024,6 @@
             }
         }
 
-        if (!vcpDeviceVolumeApiImprovements() && !isAutonomous) {
-            /* If the change is triggered by Android device, the stream is already changed.
-             * However it might be called with isAutonomous, one the first read of after
-             * reconnection. Make sure device has group volume. Also it might happen that
-             * remote side send us wrong value - lets check it.
-             */
-
-            int groupVolume = getGroupVolume(groupId);
-            Boolean groupMute = getGroupMute(groupId);
-
-            if ((groupVolume == volume) && (groupMute == mute)) {
-                Log.i(TAG, " Volume:" + volume + ", mute:" + mute + " confirmed by remote side.");
-                return;
-            }
-
-            if (device != null) {
-                // Correct the volume level only if device was already reported as connected.
-                boolean can_change_volume = false;
-                synchronized (mStateMachines) {
-                    VolumeControlStateMachine sm = mStateMachines.get(device);
-                    if (sm != null) {
-                        can_change_volume = sm.isConnected();
-                    }
-                }
-
-                if (can_change_volume && (groupVolume != volume)) {
-                    Log.i(TAG, "Setting value:" + groupVolume + " to " + device);
-                    mNativeInterface.setVolume(device, groupVolume);
-                }
-                if (can_change_volume && (groupMute != mute)) {
-                    Log.i(TAG, "Setting mute:" + groupMute + " to " + device);
-                    if (groupMute) {
-                        mNativeInterface.mute(device);
-                    } else {
-                        mNativeInterface.unmute(device);
-                    }
-                }
-            } else {
-                Log.e(
-                        TAG,
-                        "Volume changed did not succeed. Volume: "
-                                + volume
-                                + " expected volume: "
-                                + groupVolume);
-            }
-        }
-
         if (isAutonomous && device == null) {
             /* Received group notification for autonomous change. Update cache and audio system. */
             updateGroupCacheAndAudioSystem(groupId, volume, mute, true);
@@ -1565,15 +1463,8 @@
             int broadcastVolume = VOLUME_CONTROL_UNKNOWN_VOLUME;
             if (volume.isPresent()) {
                 broadcastVolume = volume.get();
-                if (!vcpDeviceVolumeApiImprovements()) {
-                    mDeviceVolumeCache.put(dev, broadcastVolume);
-                }
             } else {
                 broadcastVolume = getDeviceVolume(dev);
-                if (!vcpDeviceVolumeApiImprovements()
-                        && broadcastVolume == VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                    broadcastVolume = getGroupVolume(getGroupId(dev));
-                }
             }
             int n = callbacks.beginBroadcast();
             for (int i = 0; i < n; i++) {
@@ -1667,22 +1558,6 @@
                 Log.d(TAG, device + " is unbond. Remove state machine");
                 removeStateMachine(device);
             }
-        } else if (!vcpDeviceVolumeApiImprovements() && toState == STATE_CONNECTED) {
-            // Restore the group volume if it was changed while the device was not yet connected.
-            Integer groupId = getGroupId(device);
-            if (groupId != GROUP_ID_INVALID) {
-                Integer groupVolume = getGroupVolume(groupId);
-                if (groupVolume != VOLUME_CONTROL_UNKNOWN_VOLUME) {
-                    mNativeInterface.setVolume(device, groupVolume);
-                }
-
-                Boolean groupMute = getGroupMute(groupId);
-                if (groupMute) {
-                    mNativeInterface.mute(device);
-                } else {
-                    mNativeInterface.unmute(device);
-                }
-            }
         }
         mAdapterService.handleProfileConnectionStateChange(
                 BluetoothProfile.VOLUME_CONTROL, device, fromState, toState);
@@ -1724,17 +1599,15 @@
                             + getGroupMute(entry.getKey()));
         }
 
-        if (vcpDeviceVolumeApiImprovements()) {
-            for (Map.Entry<BluetoothDevice, Integer> entry : mDeviceVolumeCache.entrySet()) {
-                ProfileService.println(
-                        sb,
-                        "    Device: "
-                                + entry.getKey()
-                                + " volume: "
-                                + entry.getValue()
-                                + ", mute: "
-                                + getMute(entry.getKey()));
-            }
+        for (Map.Entry<BluetoothDevice, Integer> entry : mDeviceVolumeCache.entrySet()) {
+            ProfileService.println(
+                    sb,
+                    "    Device: "
+                            + entry.getKey()
+                            + " volume: "
+                            + entry.getValue()
+                            + ", mute: "
+                            + getMute(entry.getKey()));
         }
     }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
index 07b5653..c0feb6f 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
@@ -61,7 +61,6 @@
 import android.media.AudioManager;
 import android.os.Binder;
 import android.os.ParcelUuid;
-import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 
@@ -469,25 +468,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_VCP_DEVICE_VOLUME_API_IMPROVEMENTS)
-    public void volumeCacheDeprecated() {
-        int groupId = 1;
-        int volume = 6;
-
-        assertThat(mService.getGroupVolume(groupId)).isEqualTo(-1);
-        mService.setGroupVolume(groupId, volume);
-
-        assertThat(mService.getGroupVolume(groupId)).isEqualTo(volume);
-
-        volume = 10;
-        // Send autonomous volume change.
-        generateVolumeStateChanged(null, groupId, volume, 0, false, true);
-
-        assertThat(mService.getGroupVolume(groupId)).isEqualTo(volume);
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_VCP_DEVICE_VOLUME_API_IMPROVEMENTS)
     public void volumeCache() {
         int groupId = 1;
         int groupVolume = 6;
@@ -568,31 +548,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_VCP_DEVICE_VOLUME_API_IMPROVEMENTS)
-    public void muteCacheDeprecated() {
-        int groupId = 1;
-        int volume = 6;
-
-        assertThat(mService.getGroupMute(groupId)).isFalse();
-
-        // Send autonomous volume change
-        generateVolumeStateChanged(null, groupId, volume, 0, false, true);
-
-        // Mute
-        mService.muteGroup(groupId);
-        assertThat(mService.getGroupMute(groupId)).isTrue();
-
-        // Make sure the volume is kept even when muted
-        assertThat(mService.getGroupVolume(groupId)).isEqualTo(volume);
-
-        // Send autonomous unmute
-        generateVolumeStateChanged(null, groupId, volume, 0, false, true);
-
-        assertThat(mService.getGroupMute(groupId)).isFalse();
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_VCP_DEVICE_VOLUME_API_IMPROVEMENTS)
     public void muteCache() {
         int groupId = 1;
         int groupVolume = 6;
@@ -727,12 +682,10 @@
         inOrderAudio.verify(mAudioManager, never()).setStreamVolume(anyInt(), anyInt(), anyInt());
 
         InOrder inOrderNative = inOrder(mNativeInterface);
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            // AF always call setVolume via LeAudioService at first connected remote from group
-            mService.setGroupVolume(groupId, 123);
-            // It should be ignored and not set to native
-            inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt());
-        }
+        // AF always call setVolume via LeAudioService at first connected remote from group
+        mService.setGroupVolume(groupId, 123);
+        // It should be ignored and not set to native
+        inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt());
 
         // Make device Active now. This will trigger setting volume to AF
         when(mLeAudioService.getActiveGroupId()).thenReturn(groupId);
@@ -756,15 +709,10 @@
                 initialAutonomousFlag);
 
         inOrderAudio.verify(mAudioManager, never()).setStreamVolume(anyInt(), anyInt(), anyInt());
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(volumeDevice));
-        } else {
-            inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(volumeDevice));
-        }
+        inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(volumeDevice));
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_VCP_DEVICE_VOLUME_API_IMPROVEMENTS)
     public void testClearingSetVolumeFromAF() {
         int groupId = 1;
         int groupId2 = 2;
@@ -860,10 +808,8 @@
         InOrder inOrderAudio = inOrder(mAudioManager);
         inOrderAudio.verify(mAudioManager, never()).setStreamVolume(anyInt(), anyInt(), anyInt());
         InOrder inOrderNative = inOrder(mNativeInterface);
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            // AF always call setVolume via LeAudioService at first connected remote from group
-            mService.setGroupVolume(groupId, expectedAfVol);
-        }
+        // AF always call setVolume via LeAudioService at first connected remote from group
+        mService.setGroupVolume(groupId, expectedAfVol);
         inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(expectedAfVol));
 
         // Make device Active now. This will trigger setting volume to AF
@@ -886,11 +832,7 @@
                 initialAutonomousFlag);
 
         inOrderAudio.verify(mAudioManager, never()).setStreamVolume(anyInt(), anyInt(), anyInt());
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(expectedAfVol));
-        } else {
-            inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(expectedAfVol));
-        }
+        inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(expectedAfVol));
     }
 
     /** Test if phone will set volume which is read from the buds */
@@ -934,12 +876,7 @@
         assertThat(mService.getDevices()).contains(mDeviceTwo);
         generateVolumeStateChanged(mDeviceTwo, LE_AUDIO_GROUP_ID_INVALID, volume_2, 0, false, true);
 
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
-        } else {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
-            inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(groupVolume));
-        }
+        inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
     }
 
     /**
@@ -1013,14 +950,8 @@
         generateVolumeStateChanged(mDeviceTwo, LE_AUDIO_GROUP_ID_INVALID, volume_2, 0, false, true);
 
         // Check if new device was muted
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(volume));
-            inOrderNative.verify(mNativeInterface).mute(eq(mDeviceTwo));
-        } else {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(volume));
-            inOrderNative.verify(mNativeInterface).mute(eq(mDeviceTwo));
-            inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(volume));
-        }
+        inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(volume));
+        inOrderNative.verify(mNativeInterface).mute(eq(mDeviceTwo));
     }
 
     /**
@@ -1265,12 +1196,7 @@
         generateVolumeStateChanged(
                 mDeviceTwo, LE_AUDIO_GROUP_ID_INVALID, groupVolume, 0, false, true);
 
-        if (Flags.vcpDeviceVolumeApiImprovements()) {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
-        } else {
-            inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
-            inOrderNative.verify(mNativeInterface).setGroupVolume(eq(groupId), eq(groupVolume));
-        }
+        inOrderNative.verify(mNativeInterface).setVolume(eq(mDeviceTwo), eq(groupVolume));
 
         // Generate events for both devices
         generateDeviceOffsetChangedMessageFromNative(mDevice, 1, 100);
diff --git a/flags/adapter.aconfig b/flags/adapter.aconfig
index 20fd6d7..8061bce 100644
--- a/flags/adapter.aconfig
+++ b/flags/adapter.aconfig
@@ -29,16 +29,6 @@
 }
 
 flag {
-    name: "empty_names_are_invalid"
-    namespace: "bluetooth"
-    description: "Make sure the names used in the stack are valid"
-    bug: "395178934"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "remove_legacy_management_thread"
     namespace: "bluetooth"
     description: "Remove non-necessary management_thread"
diff --git a/flags/connectivity.aconfig b/flags/connectivity.aconfig
index 738c294..4896af3 100644
--- a/flags/connectivity.aconfig
+++ b/flags/connectivity.aconfig
@@ -8,3 +8,12 @@
     bug: "389274300"
 }
 
+flag {
+    name: "idempotent_direct_connect_add"
+    namespace: "bluetooth"
+    description: "Make direct_connect_add idempotent"
+    bug: "409239278"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/flags/hfp.aconfig b/flags/hfp.aconfig
index 06de4a1..3e4659c 100644
--- a/flags/hfp.aconfig
+++ b/flags/hfp.aconfig
@@ -122,3 +122,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "qc_prioritize_lc3_codec"
+    namespace: "bluetooth"
+    description: "QC patch to prioritize lc3 codec over aptx"
+    bug: "409604300"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/flags/rfcomm.aconfig b/flags/rfcomm.aconfig
index 6a4020e..a46f9f6 100644
--- a/flags/rfcomm.aconfig
+++ b/flags/rfcomm.aconfig
@@ -2,16 +2,6 @@
 container: "com.android.bt"
 
 flag {
-    name: "rfcomm_always_disc_initiator_in_disc_wait_ua"
-    namespace: "bluetooth"
-    description: "Always be the DISC initiator in the DISC_WAIT_UA state to avoid unnecessary hang"
-    bug: "350839022"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "allow_free_last_scn"
     namespace: "bluetooth"
     description: "Allow SCN 30 to be freed"
diff --git a/flags/vcp.aconfig b/flags/vcp.aconfig
index 12e55de..b862b9e 100644
--- a/flags/vcp.aconfig
+++ b/flags/vcp.aconfig
@@ -9,16 +9,6 @@
 }
 
 flag {
-    name: "vcp_device_volume_api_improvements"
-    namespace: "bluetooth"
-    description: "Refactor Device Volume API for generic usage"
-    bug: "381507732"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "vcp_allow_set_same_volume_if_pending"
     namespace: "bluetooth"
     description: "Allow to set the same volume during already pending operation"
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index 256f98f..81627d5 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -3862,7 +3862,7 @@
  ******************************************************************************/
 void btif_dm_read_energy_info() { BTA_DmBleGetEnergyInfo(bta_energy_info_cb); }
 
-static const char* btif_get_default_local_name_new() {
+static const char* btif_get_default_local_name() {
   using bluetooth::common::StringTrim;
   static std::string default_name = "";
 
@@ -3889,30 +3889,6 @@
   return default_name.c_str();
 }
 
-static const char* btif_get_default_local_name() {
-  if (com::android::bluetooth::flags::empty_names_are_invalid()) {
-    return btif_get_default_local_name_new();
-  }
-  static char btif_default_local_name[DEFAULT_LOCAL_NAME_MAX + 1] = {'\0'};
-
-  if (btif_default_local_name[0] == '\0') {
-    int max_len = sizeof(btif_default_local_name) - 1;
-
-    char prop_name[PROPERTY_VALUE_MAX];
-    osi_property_get(PROPERTY_DEFAULT_DEVICE_NAME, prop_name, "");
-    strncpy(btif_default_local_name, prop_name, max_len);
-
-    // If no value was placed in the btif_default_local_name then use model name
-    if (btif_default_local_name[0] == '\0') {
-      char prop_model[PROPERTY_VALUE_MAX];
-      osi_property_get(PROPERTY_PRODUCT_MODEL, prop_model, "");
-      strncpy(btif_default_local_name, prop_model, max_len);
-    }
-    btif_default_local_name[max_len] = '\0';
-  }
-  return btif_default_local_name;
-}
-
 static void btif_stats_add_bond_event(const RawAddress& bd_addr, bt_bond_function_t function,
                                       bt_bond_state_t state) {
   std::unique_lock<std::mutex> lock(bond_event_lock);
diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc
index b5908b0..77c8f90 100644
--- a/system/btif/src/btif_storage.cc
+++ b/system/btif/src/btif_storage.cc
@@ -255,16 +255,7 @@
         ret = btif_config_get_str(BTIF_STORAGE_SECTION_ADAPTER, BTIF_STORAGE_KEY_NAME,
                                   reinterpret_cast<char*>(prop->val), &len);
       }
-      if (com::android::bluetooth::flags::empty_names_are_invalid()) {
-        if (ret && len > 1 && len <= prop->len) {  // empty names have a len of 1
-          prop->len = len - 1;
-        } else {
-          prop->len = 0;
-          ret = false;
-        }
-        break;
-      }
-      if (ret && len && len <= prop->len) {
+      if (ret && len > 1 && len <= prop->len) {  // empty names have a len of 1
         prop->len = len - 1;
       } else {
         prop->len = 0;
diff --git a/system/common/message_loop_thread.cc b/system/common/message_loop_thread.cc
index a6dd211..5af121d 100644
--- a/system/common/message_loop_thread.cc
+++ b/system/common/message_loop_thread.cc
@@ -127,12 +127,20 @@
 
 std::string MessageLoopThread::ToString() const {
   std::lock_guard<std::recursive_mutex> api_lock(api_mutex_);
+#if defined(TARGET_FLOSS) && BASE_VER >= 1419016
+  return std::format("{}({})", thread_name_, thread_id_.raw());
+#else
   return std::format("{}({})", thread_name_, thread_id_);
+#endif // defined(TARGET_FLOSS) && BASE_VER >= 1419016
 }
 
 bool MessageLoopThread::IsRunning() const {
   std::lock_guard<std::recursive_mutex> api_lock(api_mutex_);
+#if defined(TARGET_FLOSS) && BASE_VER >= 1419016
+  return thread_id_.raw() != -1;
+#else
   return thread_id_ != -1;
+#endif // defined(TARGET_FLOSS) && BASE_VER >= 1419016
 }
 
 // Non API method, should not be protected by API mutex
@@ -187,7 +195,11 @@
 
   {
     std::lock_guard<std::recursive_mutex> api_lock(api_mutex_);
+  #if defined(TARGET_FLOSS) && BASE_VER >= 1419016
+    thread_id_ = base::PlatformThreadId(-1);
+  #else
     thread_id_ = -1;
+  #endif // defined(TARGET_FLOSS) && BASE_VER >= 1419016
     linux_tid_ = -1;
     delete message_loop_;
     message_loop_ = nullptr;
diff --git a/system/gd/hal/hci_hal_host.cc b/system/gd/hal/hci_hal_host.cc
index d81e9d0..4976258 100644
--- a/system/gd/hal/hci_hal_host.cc
+++ b/system/gd/hal/hci_hal_host.cc
@@ -245,6 +245,11 @@
               incoming_packet_callback_ == nullptr && callback != nullptr,
               "assert failed: incoming_packet_callback_ == nullptr && callback != nullptr");
       incoming_packet_callback_ = callback;
+
+      // If the HCI socket was not connected on start, inform HCI Layer to prevent future requests.
+      if (sock_fd_ == INVALID_FD) {
+        incoming_packet_callback_->controllerNeedsReset();
+      }
     }
     log::info("after");
   }
@@ -313,8 +318,8 @@
 
     // We don't want to crash when the chipset is broken.
     if (sock_fd_ == INVALID_FD) {
-      log::error("Failed to connect to HCI socket. Aborting HAL initialization process.");
-      incoming_packet_callback_->controllerNeedsReset();
+      log::error(
+              "Failed to connect to HCI socket. HAL initialization process will be aborted later.");
       return;
     }
 
diff --git a/system/gd/hal/snoop_logger_socket.cc b/system/gd/hal/snoop_logger_socket.cc
index 0405950..4ccaa72 100644
--- a/system/gd/hal/snoop_logger_socket.cc
+++ b/system/gd/hal/snoop_logger_socket.cc
@@ -67,7 +67,7 @@
   }
 
   ssize_t ret;
-  RUN_NO_INTR(ret = syscall_if_->Send(client_socket, data, length, MSG_DONTWAIT));
+  RUN_NO_INTR(ret = syscall_if_->Send(client_socket, data, length, MSG_DONTWAIT | MSG_NOSIGNAL));
 
   if (ret == -1 && syscall_if_->GetErrno() == ECONNRESET) {
     SafeCloseSocket(client_socket);
diff --git a/system/gd/hal/snoop_logger_socket_thread.cc b/system/gd/hal/snoop_logger_socket_thread.cc
index f622fb2..b8c1cdf 100644
--- a/system/gd/hal/snoop_logger_socket_thread.cc
+++ b/system/gd/hal/snoop_logger_socket_thread.cc
@@ -47,6 +47,10 @@
 std::future<bool> SnoopLoggerSocketThread::Start() {
   log::debug("");
   std::promise<bool> thread_started;
+  if (listen_thread_) {
+    thread_started.set_value(true);
+    return thread_started.get_future();
+  }
   auto future = thread_started.get_future();
   stop_thread_ = false;
   listen_thread_ = std::make_unique<std::thread>(&SnoopLoggerSocketThread::Run, this,
@@ -63,6 +67,7 @@
   if (listen_thread_ && listen_thread_->joinable()) {
     listen_thread_->join();
     listen_thread_.reset();
+    socket_->Cleanup();
   }
 }
 
@@ -87,7 +92,12 @@
   while (!stop_thread_ && socket_->ProcessIncomingRequest()) {
   }
 
-  socket_->Cleanup();
+  // We don't call `socket_->Cleanup()` here because it's possible for that to lead to SIGPIPE: in
+  // `Stop` it sets `stop_thread_` to true, and then calls `socket_->NotifySocketListener()`. Within
+  // that small window, we might have checked `stop_thread_` above, and if we were to call
+  // `socket_->Cleanup` here, that would then mean that `socket_->NotifySocketListener()` could
+  // result in SIGPIPE, which, by default will terminate the process.
+
   listen_thread_running_ = false;
 }
 
diff --git a/system/gd/hal/snoop_logger_socket_thread.h b/system/gd/hal/snoop_logger_socket_thread.h
index f508ff9..ca783ac 100644
--- a/system/gd/hal/snoop_logger_socket_thread.h
+++ b/system/gd/hal/snoop_logger_socket_thread.h
@@ -51,10 +51,8 @@
 
   // Socket thread for listening to incoming connections.
   std::unique_ptr<std::thread> listen_thread_;
-  bool listen_thread_running_ = false;
+  std::atomic<bool> listen_thread_running_ = false;
 
-  std::condition_variable listen_thread_running_cv_;
-  std::mutex listen_thread_running_mutex_;
   std::atomic<bool> stop_thread_;
 };
 
diff --git a/system/gd/hal/snoop_logger_socket_thread_test.cc b/system/gd/hal/snoop_logger_socket_thread_test.cc
index 7f0dc48..5cfa457 100644
--- a/system/gd/hal/snoop_logger_socket_thread_test.cc
+++ b/system/gd/hal/snoop_logger_socket_thread_test.cc
@@ -150,7 +150,6 @@
   sls.Stop();
 
   ASSERT_FALSE(sls.ThreadIsRunning());
-  close(socket_fd);
 }
 
 TEST_F(SnoopLoggerSocketThreadModuleTest, socket_send_no_start_test) {
diff --git a/system/metrics/Android.bp b/system/metrics/Android.bp
index 959307a..90dd3c3 100644
--- a/system/metrics/Android.bp
+++ b/system/metrics/Android.bp
@@ -128,7 +128,6 @@
     srcs: [
         ":TestCommonMockFunctions",
         "mock/metrics_mock.cc",
-        "mock/mock_main_shim_metric_id_api.cc",
         "src/bluetooth_event.cc",
         "src/metric_id_manager.cc",
     ],
diff --git a/system/metrics/mock/metrics_mock.cc b/system/metrics/mock/metrics_mock.cc
index 6895b21..4f1d23fa 100644
--- a/system/metrics/mock/metrics_mock.cc
+++ b/system/metrics/mock/metrics_mock.cc
@@ -28,6 +28,56 @@
   metricsInstance = std::move(instance);
 }
 
+bool InitMetricIdAllocator(const std::unordered_map<RawAddress, int>& paired_device_map,
+                           CallbackLegacy save_id_callback, CallbackLegacy forget_device_callback) {
+  if (metricsInstance) {
+    return metricsInstance->InitMetricIdAllocator(paired_device_map, save_id_callback,
+                                                  forget_device_callback);
+  }
+  return true;
+}
+
+bool CloseMetricIdAllocator() {
+  if (metricsInstance) {
+    return metricsInstance->CloseMetricIdAllocator();
+  }
+  return true;
+}
+
+bool IsEmptyMetricIdAllocator() {
+  if (metricsInstance) {
+    return metricsInstance->IsEmptyMetricIdAllocator();
+  }
+  return false;
+}
+
+bool IsValidIdFromMetricIdAllocator(int id) {
+  if (metricsInstance) {
+    return metricsInstance->IsValidIdFromMetricIdAllocator(id);
+  }
+  return false;
+}
+
+bool SaveDeviceOnMetricIdAllocator(const RawAddress& address) {
+  if (metricsInstance) {
+    return metricsInstance->SaveDeviceOnMetricIdAllocator(address);
+  }
+  return false;
+}
+
+int AllocateIdFromMetricIdAllocator(const RawAddress& address) {
+  if (metricsInstance) {
+    return metricsInstance->AllocateIdFromMetricIdAllocator(address);
+  }
+  return 0;
+}
+
+void ForgetDeviceFromMetricIdAllocator(const RawAddress& address) {
+  if (metricsInstance) {
+    return metricsInstance->ForgetDeviceFromMetricIdAllocator(address);
+  }
+}
+
 void LogMetricLinkLayerConnectionEvent(const hci::Address& address, uint32_t connection_handle,
                                        android::bluetooth::DirectionEnum direction,
                                        uint16_t link_type, uint32_t hci_cmd, uint16_t hci_event,
diff --git a/system/metrics/mock/metrics_mock.h b/system/metrics/mock/metrics_mock.h
index 128eba27..361d582 100644
--- a/system/metrics/mock/metrics_mock.h
+++ b/system/metrics/mock/metrics_mock.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <bluetooth/metrics/metric_id_api.h>
 #include <bluetooth/metrics/os_metrics.h>
 #include <gmock/gmock.h>
 
@@ -25,6 +26,17 @@
 public:
   static void SetInstance(std::shared_ptr<MockMetrics> instance);
 
+  // Methods from metric_id_api.h
+  MOCK_METHOD(bool, InitMetricIdAllocator,
+              ((const std::unordered_map<RawAddress, int>&), CallbackLegacy, CallbackLegacy));
+  MOCK_METHOD(bool, CloseMetricIdAllocator, ());
+  MOCK_METHOD(bool, IsEmptyMetricIdAllocator, ());
+  MOCK_METHOD(int, AllocateIdFromMetricIdAllocator, (const RawAddress&));
+  MOCK_METHOD(bool, SaveDeviceOnMetricIdAllocator, (const RawAddress&));
+  MOCK_METHOD(void, ForgetDeviceFromMetricIdAllocator, (const RawAddress&));
+  MOCK_METHOD(bool, IsValidIdFromMetricIdAllocator, (int));
+
+  // Methods from os_metrics.h
   MOCK_METHOD(void, LogMetricLinkLayerConnectionEvent,
               (const hci::Address&, uint32_t, android::bluetooth::DirectionEnum, uint16_t, uint32_t,
                uint16_t, uint16_t, uint16_t, uint16_t));
diff --git a/system/metrics/mock/mock_main_shim_metric_id_api.cc b/system/metrics/mock/mock_main_shim_metric_id_api.cc
deleted file mode 100644
index 71483d9..0000000
--- a/system/metrics/mock/mock_main_shim_metric_id_api.cc
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Generated mock file from original source file
- *   Functions generated:7
- */
-
-#include <bluetooth/metrics/metric_id_api.h>
-
-#include "test/common/mock_functions.h"
-#include "types/raw_address.h"
-
-namespace bluetooth {
-namespace metrics {
-
-bool InitMetricIdAllocator(const std::unordered_map<RawAddress, int>& /* paired_device_map */,
-                           CallbackLegacy /* save_id_callback */,
-                           CallbackLegacy /* forget_device_callback */) {
-  inc_func_call_count(__func__);
-  return true;
-}
-bool CloseMetricIdAllocator() {
-  inc_func_call_count(__func__);
-  return true;
-}
-bool IsEmptyMetricIdAllocator() {
-  inc_func_call_count(__func__);
-  return false;
-}
-bool IsValidIdFromMetricIdAllocator(const int /* id */) {
-  inc_func_call_count(__func__);
-  return false;
-}
-bool SaveDeviceOnMetricIdAllocator(const RawAddress& /* raw_address */) {
-  inc_func_call_count(__func__);
-  return false;
-}
-int AllocateIdFromMetricIdAllocator(const RawAddress& /* raw_address */) {
-  inc_func_call_count(__func__);
-  return 0;
-}
-void ForgetDeviceFromMetricIdAllocator(const RawAddress& /* raw_address */) {
-  inc_func_call_count(__func__);
-}
-
-}  // namespace metrics
-}  // namespace bluetooth
diff --git a/system/rust/src/core/shared_box.rs b/system/rust/src/core/shared_box.rs
index 33cae9f..1e1b1a1 100644
--- a/system/rust/src/core/shared_box.rs
+++ b/system/rust/src/core/shared_box.rs
@@ -21,6 +21,11 @@
         Self(t.into())
     }
 
+    /// Same as Rc::new_cyclic.
+    pub fn new_cyclic(data_fn: impl FnOnce(WeakBox<T>) -> T) -> Self {
+        Self(Rc::new_cyclic(|weak| data_fn(WeakBox(Weak::clone(weak)))))
+    }
+
     /// Produce a weak reference to the contents
     pub fn downgrade(&self) -> WeakBox<T> {
         WeakBox(Rc::downgrade(&self.0))
diff --git a/system/rust/src/gatt/callbacks.rs b/system/rust/src/gatt/callbacks.rs
index bfeb3a5..3e17f2f 100644
--- a/system/rust/src/gatt/callbacks.rs
+++ b/system/rust/src/gatt/callbacks.rs
@@ -81,7 +81,7 @@
 }
 
 /// Whether to commit or cancel a transaction
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 #[allow(dead_code)]
 pub enum TransactionDecision {
     /// Commit all pending writes
@@ -124,7 +124,6 @@
     );
 
     /// Execute or cancel any prepared writes
-    #[allow(dead_code)]
     async fn execute(
         &self,
         tcb_idx: TransportIndex,
diff --git a/system/rust/src/gatt/mocks/mock_database_callbacks.rs b/system/rust/src/gatt/mocks/mock_database_callbacks.rs
index bb50bc5..48c4cb3 100644
--- a/system/rust/src/gatt/mocks/mock_database_callbacks.rs
+++ b/system/rust/src/gatt/mocks/mock_database_callbacks.rs
@@ -2,10 +2,10 @@
 
 use std::ops::RangeInclusive;
 
-use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::core::shared_box::SharedBox;
 use crate::gatt::ids::{AttHandle, TransportIndex};
-use crate::gatt::server::att_server_bearer::AttServerBearer;
-use crate::gatt::server::gatt_database::{AttDatabaseImpl, GattDatabaseCallbacks};
+use crate::gatt::server::att_client::{AttClient, WeakAttClient};
+use crate::gatt::server::gatt_database::GattDatabaseCallbacks;
 use tokio::sync::mpsc::{self, unbounded_channel, UnboundedReceiver};
 
 /// Routes calls to GattDatabaseCallbacks into a channel of MockCallbackEvents
@@ -24,7 +24,7 @@
 pub enum MockCallbackEvents {
     /// GattDatabaseCallbacks#on_le_connect invoked
     #[allow(dead_code)]
-    OnLeConnect(TransportIndex, WeakBox<AttServerBearer<AttDatabaseImpl>>),
+    OnLeConnect(WeakAttClient),
     /// GattDatabaseCallbacks#on_le_disconnect invoked
     OnLeDisconnect(TransportIndex),
     /// GattDatabaseCallbacks#on_service_change invoked
@@ -32,12 +32,8 @@
 }
 
 impl GattDatabaseCallbacks for MockCallbacks {
-    fn on_le_connect(
-        &self,
-        tcb_idx: TransportIndex,
-        bearer: &SharedBox<AttServerBearer<AttDatabaseImpl>>,
-    ) {
-        self.0.send(MockCallbackEvents::OnLeConnect(tcb_idx, bearer.downgrade())).ok().unwrap();
+    fn on_le_connect(&self, client: &SharedBox<AttClient>) {
+        self.0.send(MockCallbackEvents::OnLeConnect(client.downgrade())).ok().unwrap();
     }
 
     fn on_le_disconnect(&self, tcb_idx: TransportIndex) {
diff --git a/system/rust/src/gatt/server.rs b/system/rust/src/gatt/server.rs
index 4d55df8..f4b60c4 100644
--- a/system/rust/src/gatt/server.rs
+++ b/system/rust/src/gatt/server.rs
@@ -1,6 +1,7 @@
 //! This module is a simple GATT server that shares the ATT channel with the
 //! existing C++ GATT client.
 
+pub mod att_client;
 mod att_database;
 pub mod att_server_bearer;
 pub mod gatt_database;
@@ -18,12 +19,13 @@
 use std::rc::Rc;
 use std::sync::{Arc, Mutex, MutexGuard};
 
-use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::core::shared_box::SharedBox;
+use crate::gatt::server::att_client::AttClient;
 use crate::gatt::server::gatt_database::GattDatabase;
 
 use self::super::ids::ServerId;
 use self::att_server_bearer::AttServerBearer;
-use self::gatt_database::{AttDatabaseImpl, GattServiceWithHandle};
+use self::gatt_database::GattServiceWithHandle;
 use self::isolation_manager::IsolationManager;
 use self::services::register_builtin_services;
 
@@ -35,9 +37,11 @@
 
 pub use indication_handler::IndicationError;
 
+type AttClients = HashMap<TransportIndex, SharedBox<AttClient>>;
+
 #[allow(missing_docs)]
 pub struct GattModule {
-    connections: HashMap<TransportIndex, GattConnection>,
+    clients: AttClients,
     databases: HashMap<ServerId, SharedBox<GattDatabase>>,
     transport: Rc<dyn AttTransport>,
     // NOTE: this is logically owned by the GattModule. We share it behind a Mutex just so we
@@ -46,23 +50,13 @@
     isolation_manager: Arc<Mutex<IsolationManager>>,
 }
 
-struct GattConnection {
-    bearer: SharedBox<AttServerBearer<AttDatabaseImpl>>,
-    database: WeakBox<GattDatabase>,
-}
-
 impl GattModule {
     /// Constructor.
     pub fn new(
         transport: Rc<dyn AttTransport>,
         isolation_manager: Arc<Mutex<IsolationManager>>,
     ) -> Self {
-        Self {
-            connections: HashMap::new(),
-            databases: HashMap::new(),
-            transport,
-            isolation_manager,
-        }
+        Self { clients: AttClients::new(), databases: HashMap::new(), transport, isolation_manager }
     }
 
     /// Handle LE link connect
@@ -83,12 +77,14 @@
         };
 
         let transport = self.transport.clone();
-        let bearer = SharedBox::new(AttServerBearer::new(
-            database.get_att_database(tcb_idx),
-            move |packet| transport.send_packet(tcb_idx, packet),
-        ));
-        database.on_bearer_ready(tcb_idx, &bearer);
-        self.connections.insert(tcb_idx, GattConnection { bearer, database: database.downgrade() });
+        self.clients.insert(
+            tcb_idx,
+            AttClient::new_client_and_bearer(
+                tcb_idx,
+                move |packet| transport.send_packet(tcb_idx, packet),
+                database,
+            ),
+        );
         Ok(())
     }
 
@@ -96,12 +92,9 @@
     pub fn on_le_disconnect(&mut self, tcb_idx: TransportIndex) -> Result<()> {
         info!("disconnected conn_id {tcb_idx:?}");
         self.isolation_manager.lock().unwrap().on_le_disconnect(tcb_idx);
-        let connection = self.connections.remove(&tcb_idx);
-        let Some(connection) = connection else {
+        if self.clients.remove(&tcb_idx).is_none() {
             bail!("got disconnection from {tcb_idx:?} but bearer does not exist");
         };
-        drop(connection.bearer);
-        connection.database.with(|db| db.map(|db| db.on_bearer_dropped(tcb_idx)));
         Ok(())
     }
 
@@ -157,11 +150,8 @@
     }
 
     /// Get an ATT bearer for a particular connection
-    pub fn get_bearer(
-        &self,
-        tcb_idx: TransportIndex,
-    ) -> Option<&SharedBox<AttServerBearer<AttDatabaseImpl>>> {
-        self.connections.get(&tcb_idx).map(|x| &x.bearer)
+    pub fn get_bearer(&self, tcb_idx: TransportIndex) -> Option<&SharedBox<AttServerBearer>> {
+        self.clients.get(&tcb_idx).map(|c| c.bearer())
     }
 
     /// Get the IsolationManager to manage associations between servers + advertisers
diff --git a/system/rust/src/gatt/server/att_client.rs b/system/rust/src/gatt/server/att_client.rs
new file mode 100644
index 0000000..ee561ff
--- /dev/null
+++ b/system/rust/src/gatt/server/att_client.rs
@@ -0,0 +1,361 @@
+use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::gatt::callbacks::{GattWriteRequestType, RawGattDatastore, TransactionDecision};
+use crate::gatt::ffi::AttributeBackingType;
+use crate::gatt::ids::{AttHandle, TransportIndex};
+use crate::gatt::server::att_database::AttAttribute;
+use crate::gatt::server::att_server_bearer::AttServerBearer;
+use crate::gatt::server::gatt_database::{
+    AttAttributeBackingValue, AttAttributeWithBackingValue, GattDatabase,
+};
+use crate::packets::att::{self, AttErrorCode};
+use log::{error, warn};
+use pdl_runtime::EncodeError;
+use std::cell::RefCell;
+#[cfg(test)]
+use std::future::Future;
+use std::rc::Rc;
+
+/// AttClient represents a connection from a *remote* device to a *local* server i.e. it is for
+/// *inbound* connections but it holds the required state for our server. All connections with the
+/// same remote address map to the same client, and there is a one-to-one mapping to the transport
+/// index which identifies the client in the legacy stack. For now, we only support a single bearer
+/// but in future, with EATT, there may be more bearers. A client has access to only one particular
+/// database (which may be shared amongst many clients).
+pub struct AttClient {
+    // The identifier for the client in the legacy stack.
+    tcb_idx: TransportIndex,
+
+    bearer: SharedBox<AttServerBearer>,
+
+    // The underlying database for this client. This is weak because databases can be destroyed
+    // independently of clients.
+    gatt_db: WeakBox<GattDatabase>,
+
+    // Prepared writes all have to be for the same datastore because we are unable to atomically
+    // execute writes across different datastores.
+    datastore_for_prepared_writes: RefCell<Option<Rc<dyn RawGattDatastore>>>,
+}
+
+impl AttClient {
+    pub fn new_client_and_bearer(
+        tcb_idx: TransportIndex,
+        send_packet: impl Fn(att::Att) -> Result<(), EncodeError> + 'static,
+        db: &SharedBox<GattDatabase>,
+    ) -> SharedBox<Self> {
+        let this = SharedBox::new_cyclic(|weak| Self {
+            tcb_idx,
+            bearer: SharedBox::new(AttServerBearer::new(weak, send_packet)),
+            gatt_db: db.downgrade(),
+            datastore_for_prepared_writes: RefCell::default(),
+        });
+        db.on_client_connect(&this);
+        this
+    }
+
+    /// Returns a test client and a receiver for all packets sent.
+    #[cfg(test)]
+    pub fn new_test_client(
+        tcb_idx: TransportIndex,
+        db: &SharedBox<GattDatabase>,
+    ) -> (SharedBox<Self>, tokio::sync::mpsc::UnboundedReceiver<att::Att>) {
+        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
+        (
+            Self::new_client_and_bearer(
+                tcb_idx,
+                move |packet| {
+                    tx.send(packet).unwrap();
+                    Ok(())
+                },
+                db,
+            ),
+            rx,
+        )
+    }
+
+    pub fn tcb_idx(&self) -> TransportIndex {
+        self.tcb_idx
+    }
+
+    pub fn bearer(&self) -> &SharedBox<AttServerBearer> {
+        &self.bearer
+    }
+
+    pub fn list_attributes(&self) -> Vec<AttAttribute> {
+        self.gatt_db.with(|db| db.map(|db| db.list_attributes()).unwrap_or_default())
+    }
+
+    pub fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]) {
+        if self.gatt_db.with(|db| {
+            db.and_then(|db| db.with_attribute(handle, |attr| {
+                if !attr.attribute.permissions.writable_without_response() {
+                    warn!("trying to write without response to {handle:?}, which doesn't support it");
+                    return;
+                }
+                match &attr.value {
+                    AttAttributeBackingValue::Static(val) => {
+                        error!("A static attribute {val:?} is marked as writable - ignoring it and rejecting the write...");
+                    }
+                    AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
+                        datastore.write_no_response(
+                            self.tcb_idx,
+                            handle,
+                            AttributeBackingType::Characteristic,
+                            data,
+                        );
+                    }
+                    AttAttributeBackingValue::DynamicDescriptor(datastore) => {
+                        datastore.write_no_response(
+                            self.tcb_idx,
+                            handle,
+                            AttributeBackingType::Descriptor,
+                            data,
+                        );
+                    }
+                }
+            }))}).is_none() {
+            warn!("cannot find handle {handle:?}");
+        }
+    }
+}
+
+impl Drop for AttClient {
+    fn drop(&mut self) {
+        self.gatt_db.with(|db| {
+            if let Some(db) = db {
+                db.on_client_dropped(self.tcb_idx);
+            }
+        });
+    }
+}
+
+impl SharedBox<AttClient> {
+    #[cfg(test)]
+    pub fn read_attribute(
+        &self,
+        handle: AttHandle,
+    ) -> impl Future<Output = Result<Vec<u8>, AttErrorCode>> {
+        let this = self.downgrade();
+        async move { this.read_attribute(handle).await }
+    }
+
+    #[cfg(test)]
+    pub fn write_attribute(
+        &self,
+        handle: AttHandle,
+        data: &[u8],
+    ) -> impl Future<Output = Result<(), AttErrorCode>> {
+        let this = self.downgrade();
+        let data = data.to_vec();
+        async move { this.write_attribute(handle, &data).await }
+    }
+}
+
+// Strong references to AttClient should not be held over await points (since that has the potential
+// to lead to reference cycles), so async methods operate need to operate on `WeakAttClient`.
+pub type WeakAttClient = WeakBox<AttClient>;
+
+impl WeakAttClient {
+    /// Calls `f` with the client and database. Returns an `InvalidHandle` error if either the
+    /// client or database has been closed.
+    fn with_db<T, E: From<AttErrorCode>>(
+        &self,
+        f: impl FnOnce(&AttClient, &GattDatabase) -> Result<T, E>,
+    ) -> Result<T, E> {
+        self.with(|client| {
+            client.map_or(Err(AttErrorCode::InvalidHandle.into()), |client| {
+                client.gatt_db.with(|db| {
+                    db.map_or(Err(AttErrorCode::InvalidHandle.into()), |db| f(client, db))
+                })
+            })
+        })
+    }
+
+    /// Calls `f` with the client and attribute. Returns an `InvalidHandle` error if either the
+    /// client or database has been closed, or if the handle doesn't exist.
+    pub fn with_attribute<T, E: From<AttErrorCode>>(
+        &self,
+        handle: AttHandle,
+        f: impl FnOnce(&AttClient, &AttAttributeWithBackingValue) -> Result<T, E>,
+    ) -> Result<T, E> {
+        self.with_db(|client, db| {
+            db.with_attribute(handle, |attr| f(client, attr))
+                .unwrap_or(Err(AttErrorCode::InvalidHandle.into()))
+        })
+    }
+
+    pub async fn read_attribute(&self, handle: AttHandle) -> Result<Vec<u8>, AttErrorCode> {
+        let (tcb_idx, value) = self.with_attribute(handle, |client, attr| {
+            if !attr.attribute.permissions.readable() {
+                Err(AttErrorCode::ReadNotPermitted)
+            } else {
+                Ok((client.tcb_idx, attr.value.clone()))
+            }
+        })?;
+
+        match value {
+            AttAttributeBackingValue::Static(val) => Ok(val),
+            AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
+                datastore
+                    .read(
+                        tcb_idx,
+                        handle,
+                        /* offset */ 0,
+                        AttributeBackingType::Characteristic,
+                    )
+                    .await
+            }
+            AttAttributeBackingValue::DynamicDescriptor(datastore) => {
+                datastore
+                    .read(tcb_idx, handle, /* offset */ 0, AttributeBackingType::Descriptor)
+                    .await
+            }
+        }
+    }
+
+    pub async fn write_attribute(
+        &self,
+        handle: AttHandle,
+        data: &[u8],
+    ) -> Result<(), AttErrorCode> {
+        let (tcb_idx, value) = self.with_attribute(handle, |client, attr| {
+            if !attr.attribute.permissions.writable_with_response() {
+                Err(AttErrorCode::WriteNotPermitted)
+            } else {
+                Ok((client.tcb_idx, attr.value.clone()))
+            }
+        })?;
+
+        match value {
+            AttAttributeBackingValue::Static(val) => {
+                error!(
+                    "A static attribute {val:?} (handle={handle:?}) is marked as writable - \
+                     ignoring it and rejecting the write..."
+                );
+                Err(AttErrorCode::WriteNotPermitted)
+            }
+            AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
+                datastore
+                    .write(
+                        tcb_idx,
+                        handle,
+                        AttributeBackingType::Characteristic,
+                        GattWriteRequestType::Request,
+                        data,
+                    )
+                    .await
+            }
+            AttAttributeBackingValue::DynamicDescriptor(datastore) => {
+                datastore
+                    .write(
+                        tcb_idx,
+                        handle,
+                        AttributeBackingType::Descriptor,
+                        GattWriteRequestType::Request,
+                        data,
+                    )
+                    .await
+            }
+        }
+    }
+
+    /// Returns the list of attributes or an empty list if either the client or database has been
+    /// closed. This isn't async, but it exists as a convenience method since this often needs to be
+    /// called on `WeakAttClient`.
+    pub fn list_attributes(&self) -> Vec<AttAttribute> {
+        self.with(|client| client.map(|c| c.list_attributes()).unwrap_or_default())
+    }
+
+    pub fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]) {
+        self.with(|client| {
+            if let Some(client) = client {
+                client.write_no_response_attribute(handle, data);
+            }
+        });
+    }
+
+    /// Queues a write for later execution.  As per the Bluetooth Core Specification v6.0, Vol 3,
+    /// Part F, 3.4.6.1, the offset and data length are not validated until execution time.
+    pub async fn prepare_write_attribute(
+        &self,
+        handle: AttHandle,
+        offset: u32,
+        data: &[u8],
+    ) -> Result<(), AttErrorCode> {
+        let (tcb_idx, backing_type, datastore) = self.with_attribute(handle, |client, attr| {
+            if !attr.attribute.permissions.writable_with_response() {
+                return Err(AttErrorCode::WriteNotPermitted);
+            }
+
+            let (backing_type, datastore) = match &attr.value {
+                AttAttributeBackingValue::Static(val) => {
+                    error!(
+                        "A static attribute {val:?} is marked as writable - ignoring it and \
+                            rejecting the write..."
+                    );
+                    return Err(AttErrorCode::WriteNotPermitted);
+                }
+                AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
+                    (AttributeBackingType::Characteristic, Rc::clone(datastore))
+                }
+                AttAttributeBackingValue::DynamicDescriptor(datastore) => {
+                    (AttributeBackingType::Descriptor, Rc::clone(datastore))
+                }
+            };
+
+            // We only support writing attributes within the same data-store as already queued
+            // writes (since there is no way for us to atomically update attributes across different
+            // stores). We check for equality using pointer comparisons: in practice there is a
+            // single, separate, datastore for each service.
+            match &mut *client.datastore_for_prepared_writes.borrow_mut() {
+                Some(existing_datastore) => {
+                    if !Rc::ptr_eq(existing_datastore, &datastore) {
+                        return Err(AttErrorCode::RequestNotSupported);
+                    }
+                }
+                v @ None => {
+                    // NOTE: If the prepare fails below, we won't unset the datastore which means
+                    // that if there is a subsequent attempt to write to a different datastore, it
+                    // will fail with `RequestNotSupported` even though there might not be actually
+                    // any write queued. Supporting this unlikely edge case is tricky (e.g. consider
+                    // how to make it work if writes are queued concurrently over different bearers)
+                    // and deemed not worth the effort.
+                    *v = Some(Rc::clone(&datastore));
+                }
+            }
+
+            Ok((client.tcb_idx, backing_type, datastore))
+        })?;
+
+        datastore
+            .write(tcb_idx, handle, backing_type, GattWriteRequestType::Prepare { offset }, data)
+            .await?;
+
+        Ok(())
+    }
+
+    /// Executes the queue of prepared writes.
+    pub async fn execute(
+        &self,
+        decision: TransactionDecision,
+    ) -> Result<(), (AttHandle, AttErrorCode)> {
+        match self.with(|client| {
+            if let Some(client) = client {
+                Ok(client
+                    .datastore_for_prepared_writes
+                    .borrow_mut()
+                    .take()
+                    .map(|datastore| (client.tcb_idx, datastore)))
+            } else {
+                // The client has gone away.
+                Err((AttHandle(0), AttErrorCode::UnlikelyError))
+            }
+        })? {
+            Some((tcb_idx, datastore)) => {
+                // NOTE: the handle in error isn't currently plumbed through, so we have to just
+                // use zero for now.
+                datastore.execute(tcb_idx, decision).await.map_err(|c| (AttHandle(0), c))
+            }
+            None => Ok(()),
+        }
+    }
+}
diff --git a/system/rust/src/gatt/server/att_database.rs b/system/rust/src/gatt/server/att_database.rs
index bc171a8..01b3195 100644
--- a/system/rust/src/gatt/server/att_database.rs
+++ b/system/rust/src/gatt/server/att_database.rs
@@ -1,9 +1,8 @@
-use async_trait::async_trait;
 use bitflags::bitflags;
 
 use crate::core::uuid::Uuid;
 use crate::gatt::ids::AttHandle;
-use crate::packets::att::{self, AttErrorCode};
+use crate::packets::att;
 
 impl From<att::AttHandle> for AttHandle {
     fn from(value: att::AttHandle) -> Self {
@@ -61,66 +60,3 @@
         self.contains(AttPermissions::INDICATE)
     }
 }
-
-#[async_trait(?Send)]
-pub trait AttDatabase {
-    /// Read an attribute by handle
-    async fn read_attribute(&self, handle: AttHandle) -> Result<Vec<u8>, AttErrorCode>;
-
-    /// Write to an attribute by handle
-    async fn write_attribute(&self, handle: AttHandle, data: &[u8]) -> Result<(), AttErrorCode>;
-
-    /// Write to an attribute by handle
-    fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]);
-
-    /// List all the attributes in this database.
-    ///
-    /// Expected to return them in sorted order.
-    fn list_attributes(&self) -> Vec<AttAttribute>;
-
-    /// Produce an implementation of StableAttDatabase
-    fn snapshot(&self) -> SnapshottedAttDatabase<'_>
-    where
-        Self: Sized,
-    {
-        SnapshottedAttDatabase { attributes: self.list_attributes(), backing: self }
-    }
-}
-
-/// Marker trait indicating that the backing attribute list of this
-/// database is guaranteed to remain unchanged across async points.
-///
-/// Useful if we want to call list_attributes() multiple times, rather than
-/// caching its result the first time.
-pub trait StableAttDatabase: AttDatabase {
-    fn find_attribute(&self, handle: AttHandle) -> Option<AttAttribute> {
-        self.list_attributes().into_iter().find(|attr| attr.handle == handle)
-    }
-}
-
-/// A snapshot of an AttDatabase implementing StableAttDatabase.
-pub struct SnapshottedAttDatabase<'a> {
-    attributes: Vec<AttAttribute>,
-    backing: &'a (dyn AttDatabase),
-}
-
-#[async_trait(?Send)]
-impl AttDatabase for SnapshottedAttDatabase<'_> {
-    async fn read_attribute(&self, handle: AttHandle) -> Result<Vec<u8>, AttErrorCode> {
-        self.backing.read_attribute(handle).await
-    }
-
-    async fn write_attribute(&self, handle: AttHandle, data: &[u8]) -> Result<(), AttErrorCode> {
-        self.backing.write_attribute(handle, data).await
-    }
-
-    fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]) {
-        self.backing.write_no_response_attribute(handle, data);
-    }
-
-    fn list_attributes(&self) -> Vec<AttAttribute> {
-        self.attributes.clone()
-    }
-}
-
-impl StableAttDatabase for SnapshottedAttDatabase<'_> {}
diff --git a/system/rust/src/gatt/server/att_server_bearer.rs b/system/rust/src/gatt/server/att_server_bearer.rs
index 20b138e..17b4a91 100644
--- a/system/rust/src/gatt/server/att_server_bearer.rs
+++ b/system/rust/src/gatt/server/att_server_bearer.rs
@@ -1,6 +1,6 @@
 //! This module handles an individual connection on the ATT fixed channel.
 //! It handles ATT transactions and unacknowledged operations, backed by an
-//! AttDatabase (that may in turn be backed by an upper-layer protocol)
+//! database (that may in turn be backed by an upper-layer protocol)
 
 use pdl_runtime::EncodeError;
 use std::cell::Cell;
@@ -10,21 +10,21 @@
 use log::{error, trace, warn};
 use tokio::task::spawn_local;
 
-use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::core::shared_box::SharedBox;
 use crate::core::shared_mutex::SharedMutex;
 use crate::gatt::ids::AttHandle;
 use crate::gatt::mtu::{AttMtu, MtuEvent};
 use crate::gatt::opcode_types::{AttRequest, AttType};
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att::{self, AttErrorCode};
 use crate::utils::owned_handle::OwnedHandle;
 
-use super::att_database::AttDatabase;
 use super::command_handler::AttCommandHandler;
 use super::indication_handler::{ConfirmationWatcher, IndicationError, IndicationHandler};
 use super::request_handler::AttRequestHandler;
 
-enum AttRequestState<T: AttDatabase> {
-    Idle(AttRequestHandler<T>),
+enum AttRequestState {
+    Idle(AttRequestHandler),
     Pending { _task: OwnedHandle<()> },
     Replacing,
 }
@@ -42,46 +42,49 @@
 /// This represents a single ATT bearer (currently, always the unenhanced fixed
 /// channel on LE) The AttRequestState ensures that only one transaction can
 /// take place at a time
-pub struct AttServerBearer<T: AttDatabase> {
+pub struct AttServerBearer {
     // general
     send_packet: Box<dyn Fn(att::Att) -> Result<(), EncodeError>>,
     mtu: AttMtu,
 
     // request state
-    curr_request: Cell<AttRequestState<T>>,
+    curr_request: Cell<AttRequestState>,
 
     // indication state
-    indication_handler: SharedMutex<IndicationHandler<T>>,
+    indication_handler: SharedMutex<IndicationHandler>,
     pending_confirmation: ConfirmationWatcher,
 
     // command handler (across all bearers)
-    command_handler: AttCommandHandler<T>,
+    command_handler: AttCommandHandler,
 }
 
-impl<T: AttDatabase + Clone + 'static> AttServerBearer<T> {
-    /// Constructor, wrapping an ATT channel (for outgoing packets) and an
-    /// AttDatabase
-    pub fn new(db: T, send_packet: impl Fn(att::Att) -> Result<(), EncodeError> + 'static) -> Self {
-        let (indication_handler, pending_confirmation) = IndicationHandler::new(db.clone());
+impl AttServerBearer {
+    /// Constructor, wrapping an ATT channel (for outgoing packets).
+    pub fn new(
+        client: WeakAttClient,
+        send_packet: impl Fn(att::Att) -> Result<(), EncodeError> + 'static,
+    ) -> Self {
+        let (indication_handler, pending_confirmation) = IndicationHandler::new(client.clone());
         Self {
             send_packet: Box::new(send_packet),
             mtu: AttMtu::new(),
 
-            curr_request: AttRequestState::Idle(AttRequestHandler::new(db.clone())).into(),
+            curr_request: AttRequestState::Idle(AttRequestHandler::new(client.clone())).into(),
 
             indication_handler: SharedMutex::new(indication_handler),
             pending_confirmation,
 
-            command_handler: AttCommandHandler::new(db),
+            command_handler: AttCommandHandler::new(client),
         }
     }
 
-    fn send_packet(&self, packet: att::Att) -> Result<(), EncodeError> {
+    /// Sends the specified packet on the bearer.
+    pub fn send_packet(&self, packet: att::Att) -> Result<(), EncodeError> {
         (self.send_packet)(packet)
     }
 }
 
-impl<T: AttDatabase + Clone + 'static> SharedBox<AttServerBearer<T>> {
+impl SharedBox<AttServerBearer> {
     /// Handle an incoming packet, and send outgoing packets as appropriate
     /// using the owned ATT channel.
     pub fn handle_packet(&self, packet: att::Att) {
@@ -107,7 +110,6 @@
 
         let locked_indication_handler = self.indication_handler.lock();
         let pending_mtu = self.mtu.snapshot();
-        let this = self.downgrade();
 
         async move {
             // first wait until we are at the head of the queue and are ready to send
@@ -126,7 +128,7 @@
                     IndicationError::SendError(SendError::ConnectionDropped)
                 })?;
             // finally, send, and wait for a response
-            indication_handler.send(handle, &data, mtu, |packet| this.try_send_packet(packet)).await
+            indication_handler.send(handle, &data, mtu).await
         }
     }
 
@@ -185,25 +187,12 @@
     }
 }
 
-impl<T: AttDatabase + Clone + 'static> WeakBox<AttServerBearer<T>> {
-    fn try_send_packet(&self, packet: att::Att) -> Result<(), SendError> {
-        self.with(|this| {
-            this.ok_or_else(|| {
-                warn!("connection dropped before packet sent");
-                SendError::ConnectionDropped
-            })?
-            .send_packet(packet)
-            .map_err(SendError::SerializeError)
-        })
-    }
-}
-
 #[cfg(test)]
 mod test {
     use std::rc::Rc;
 
     use tokio::sync::mpsc::error::TryRecvError;
-    use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
+    use tokio::sync::mpsc::UnboundedReceiver;
 
     use super::*;
 
@@ -212,11 +201,12 @@
     use crate::gatt::ffi::AttributeBackingType;
     use crate::gatt::ids::TransportIndex;
     use crate::gatt::mocks::mock_datastore::{MockDatastore, MockDatastoreEvents};
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::{AttAttribute, AttPermissions};
     use crate::gatt::server::gatt_database::{
         GattCharacteristicWithHandle, GattDatabase, GattServiceWithHandle,
     };
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
     use crate::utils::task::{block_on_locally, try_await};
 
@@ -227,8 +217,8 @@
     const TCB_IDX: TransportIndex = TransportIndex(1);
 
     fn open_connection(
-    ) -> (SharedBox<AttServerBearer<TestAttDatabase>>, UnboundedReceiver<att::Att>) {
-        let db = TestAttDatabase::new(vec![
+    ) -> (SharedBox<GattDatabase>, SharedBox<AttClient>, UnboundedReceiver<att::Att>) {
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: VALID_HANDLE,
@@ -246,19 +236,15 @@
                 vec![5, 6],
             ),
         ]);
-        let (tx, rx) = unbounded_channel();
-        let conn = AttServerBearer::new(db, move |packet| {
-            tx.send(packet).unwrap();
-            Ok(())
-        })
-        .into();
-        (conn, rx)
+        let (client, rx) = AttClient::new_test_client(TCB_IDX, &db);
+        (db, client, rx)
     }
 
     #[test]
     fn test_single_transaction() {
         block_on_locally(async {
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             conn.handle_packet(
                 att::AttReadRequest { attribute_handle: VALID_HANDLE.into() }.try_into().unwrap(),
             );
@@ -270,7 +256,8 @@
     #[test]
     fn test_sequential_transactions() {
         block_on_locally(async {
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             conn.handle_packet(
                 att::AttReadRequest { attribute_handle: INVALID_HANDLE.into() }.try_into().unwrap(),
             );
@@ -314,12 +301,8 @@
             datastore,
         )
         .unwrap();
-        let (tx, mut rx) = unbounded_channel();
-        let send_packet = move |packet| {
-            tx.send(packet).unwrap();
-            Ok(())
-        };
-        let conn = SharedBox::new(AttServerBearer::new(db.get_att_database(TCB_IDX), send_packet));
+        let (client, mut rx) = AttClient::new_test_client(TCB_IDX, &db);
+        let conn = client.bearer();
         let data = [1, 2];
 
         // act: send two read requests before replying to either read
@@ -357,7 +340,8 @@
     fn test_indication_confirmation() {
         block_on_locally(async {
             // arrange
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
 
             // act: send an indication
             let pending_send = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
@@ -375,7 +359,8 @@
     fn test_sequential_indications() {
         block_on_locally(async {
             // arrange
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
 
             // act: send the first indication
             let pending_send1 = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
@@ -404,7 +389,8 @@
     fn test_queued_indications_only_one_sent() {
         block_on_locally(async {
             // arrange
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
 
             // act: send two indications simultaneously
             let pending_send1 = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
@@ -423,7 +409,8 @@
     fn test_queued_indications_dequeue_second() {
         block_on_locally(async {
             // arrange
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
 
             // act: send two indications simultaneously
             let pending_send1 = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
@@ -451,7 +438,8 @@
     fn test_queued_indications_complete_both() {
         block_on_locally(async {
             // arrange
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
 
             // act: send two indications simultaneously
             let pending_send1 = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
@@ -480,12 +468,13 @@
     fn test_indication_connection_drop() {
         block_on_locally(async {
             // arrange: a pending indication
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             let pending_send = spawn_local(conn.send_indication(VALID_HANDLE, vec![1, 2, 3]));
 
             // act: drop the connection after the indication is sent
             rx.recv().await.unwrap();
-            drop(conn);
+            drop(client);
 
             // assert: the pending indication fails with the appropriate error
             assert!(matches!(
@@ -499,7 +488,8 @@
     fn test_single_indication_pending_mtu() {
         block_on_locally(async {
             // arrange: pending MTU negotiation
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             conn.handle_mtu_event(MtuEvent::OutgoingRequest).unwrap();
 
             // act: try to send an indication with a large payload size
@@ -516,7 +506,8 @@
     fn test_single_indication_pending_mtu_fail() {
         block_on_locally(async {
             // arrange: pending MTU negotiation
-            let (conn, _) = open_connection();
+            let (_db, client, _rx) = open_connection();
+            let conn = client.bearer();
             conn.handle_mtu_event(MtuEvent::OutgoingRequest).unwrap();
 
             // act: try to send an indication with a large payload size
@@ -534,7 +525,8 @@
     fn test_server_transaction_pending_mtu() {
         block_on_locally(async {
             // arrange: pending MTU negotiation
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             conn.handle_mtu_event(MtuEvent::OutgoingRequest).unwrap();
 
             // act: send server packet
@@ -551,7 +543,8 @@
     fn test_queued_indication_pending_mtu_uses_mtu_on_dequeue() {
         block_on_locally(async {
             // arrange: an outstanding indication
-            let (conn, mut rx) = open_connection();
+            let (_db, client, mut rx) = open_connection();
+            let conn = client.bearer();
             let _ = try_await(conn.send_indication(VALID_HANDLE, vec![1, 2, 3])).await;
             rx.recv().await.unwrap(); // flush rx_queue
 
diff --git a/system/rust/src/gatt/server/command_handler.rs b/system/rust/src/gatt/server/command_handler.rs
index 36ccb25..2be705d 100644
--- a/system/rust/src/gatt/server/command_handler.rs
+++ b/system/rust/src/gatt/server/command_handler.rs
@@ -1,29 +1,27 @@
 use log::warn;
 
 use crate::gatt::opcode_types::AttCommand;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att;
 
-use super::att_database::AttDatabase;
-
 /// This struct handles all ATT commands.
-pub struct AttCommandHandler<Db: AttDatabase> {
-    db: Db,
+pub struct AttCommandHandler {
+    client: WeakAttClient,
 }
 
-impl<Db: AttDatabase> AttCommandHandler<Db> {
-    pub fn new(db: Db) -> Self {
-        Self { db }
+impl AttCommandHandler {
+    pub fn new(client: WeakAttClient) -> Self {
+        Self { client }
     }
 
     pub fn process_packet(&self, packet: AttCommand) {
-        let snapshotted_db = self.db.snapshot();
         match packet.opcode {
             att::AttOpcode::WriteCommand => {
                 let Ok(packet) = att::AttWriteCommand::try_from(&*packet) else {
                     warn!("failed to parse WRITE_COMMAND packet");
                     return;
                 };
-                snapshotted_db.write_no_response_attribute(packet.handle.into(), &packet.value);
+                self.client.write_no_response_attribute(packet.handle.into(), &packet.value);
             }
             _ => {
                 warn!("Dropping unsupported opcode {:?}", packet.opcode);
@@ -35,19 +33,22 @@
 #[cfg(test)]
 mod test {
     use crate::core::uuid::Uuid;
-    use crate::gatt::ids::AttHandle;
+    use crate::gatt::ids::{AttHandle, TransportIndex};
     use crate::gatt::opcode_types::AttCommand;
-    use crate::gatt::server::att_database::{AttAttribute, AttDatabase};
+    use crate::gatt::server::att_client::AttClient;
+    use crate::gatt::server::att_database::AttAttribute;
     use crate::gatt::server::command_handler::AttCommandHandler;
     use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
     use crate::utils::task::block_on_locally;
 
+    const TCB_IDX: TransportIndex = TransportIndex(1);
+
     #[test]
     fn test_write_command() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -55,7 +56,8 @@
             },
             vec![1, 2, 3],
         )]);
-        let handler = AttCommandHandler { db: db.clone() };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let handler = AttCommandHandler::new(client.downgrade());
         let data = [1, 2];
 
         // act: send write command
@@ -67,14 +69,15 @@
         handler.process_packet(att_view);
 
         // assert: the db has been updated
-        assert_eq!(block_on_locally(db.read_attribute(AttHandle(3))).unwrap(), data);
+        assert_eq!(block_on_locally(client.read_attribute(AttHandle(3))).unwrap(), data);
     }
 
     #[test]
     fn test_unsupported_command() {
         // arrange
-        let db = TestAttDatabase::new(vec![]);
-        let handler = AttCommandHandler { db };
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let handler = AttCommandHandler::new(client.downgrade());
 
         // act: send a packet that should not be handled here
         let att_view = AttCommand::new(att::AttSignedWriteCommand {
diff --git a/system/rust/src/gatt/server/gatt_database.rs b/system/rust/src/gatt/server/gatt_database.rs
index 7d3704f..6aa7258 100644
--- a/system/rust/src/gatt/server/gatt_database.rs
+++ b/system/rust/src/gatt/server/gatt_database.rs
@@ -9,18 +9,15 @@
 use std::rc::Rc;
 
 use anyhow::{bail, Result};
-use async_trait::async_trait;
-use log::{error, warn};
 
-use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::core::shared_box::SharedBox;
 use crate::core::uuid::Uuid;
-use crate::gatt::callbacks::{GattWriteRequestType, RawGattDatastore};
-use crate::gatt::ffi::AttributeBackingType;
+use crate::gatt::callbacks::RawGattDatastore;
 use crate::gatt::ids::{AttHandle, TransportIndex};
-use crate::packets::att::{self, AttErrorCode};
+use crate::gatt::server::att_client::AttClient;
+use crate::packets::att;
 
-use super::att_database::{AttAttribute, AttDatabase};
-use super::att_server_bearer::AttServerBearer;
+use super::att_database::AttAttribute;
 
 pub use super::att_database::AttPermissions;
 
@@ -75,8 +72,8 @@
     pub permissions: AttPermissions,
 }
 
-/// The GattDatabase implements AttDatabase, and converts attribute reads/writes
-/// into GATT operations to be sent to the upper layers
+/// The GattDatabase converts attribute reads/writes into GATT operations to be sent to the upper
+/// layers.
 #[derive(Default)]
 pub struct GattDatabase {
     schema: RefCell<GattDatabaseSchema>,
@@ -89,16 +86,16 @@
 }
 
 #[derive(Clone)]
-enum AttAttributeBackingValue {
+pub enum AttAttributeBackingValue {
     Static(Vec<u8>),
     DynamicCharacteristic(Rc<dyn RawGattDatastore>),
     DynamicDescriptor(Rc<dyn RawGattDatastore>),
 }
 
 #[derive(Clone)]
-struct AttAttributeWithBackingValue {
-    attribute: AttAttribute,
-    value: AttAttributeBackingValue,
+pub struct AttAttributeWithBackingValue {
+    pub attribute: AttAttribute,
+    pub value: AttAttributeBackingValue,
 }
 
 /// Callbacks that can be registered on the GattDatabase to watch for
@@ -110,11 +107,7 @@
 /// will NEVER be invoked.
 pub trait GattDatabaseCallbacks {
     /// A peer device on the given bearer has connected to this database (and can see its attributes)
-    fn on_le_connect(
-        &self,
-        tcb_idx: TransportIndex,
-        bearer: &SharedBox<AttServerBearer<AttDatabaseImpl>>,
-    );
+    fn on_le_connect(&self, client: &SharedBox<AttClient>);
     /// A peer device has disconnected from this database
     fn on_le_disconnect(&self, tcb_idx: TransportIndex);
     /// The attributes in the specified range have changed
@@ -127,6 +120,26 @@
         Default::default()
     }
 
+    /// Returns access to the attributes in this database.
+    pub fn with_attribute<T>(
+        &self,
+        handle: AttHandle,
+        f: impl FnOnce(&AttAttributeWithBackingValue) -> T,
+    ) -> Option<T> {
+        self.schema.borrow().attributes.get(&handle).map(f)
+    }
+
+    /// Returns the specified attribute.
+    #[cfg(test)]
+    pub fn get(&self, handle: AttHandle) -> Option<AttAttributeWithBackingValue> {
+        self.schema.borrow().attributes.get(&handle).cloned()
+    }
+
+    /// Returns the attribute handles (without access to the backing values).
+    pub fn list_attributes(&self) -> Vec<AttAttribute> {
+        self.schema.borrow().attributes.values().map(|attr| attr.attribute).collect()
+    }
+
     /// Register an event listener
     pub fn register_listener(&self, callbacks: Rc<dyn GattDatabaseCallbacks>) {
         self.listeners.borrow_mut().push(callbacks);
@@ -134,39 +147,38 @@
 
     /// When a connection has been made with access to this database.
     /// The supplied bearer is guaranteed to be ready for use.
-    pub fn on_bearer_ready(
-        &self,
-        tcb_idx: TransportIndex,
-        bearer: &SharedBox<AttServerBearer<AttDatabaseImpl>>,
-    ) {
+    pub fn on_client_connect(&self, client: &SharedBox<AttClient>) {
         for listener in self.listeners.borrow().iter() {
-            listener.on_le_connect(tcb_idx, bearer);
+            listener.on_le_connect(client);
         }
     }
 
     /// When the connection has dropped.
-    pub fn on_bearer_dropped(&self, tcb_idx: TransportIndex) {
+    pub fn on_client_dropped(&self, tcb_idx: TransportIndex) {
         for listener in self.listeners.borrow().iter() {
             listener.on_le_disconnect(tcb_idx);
         }
     }
 
-    /// Add a service with pre-allocated handles (for co-existence with C++) backed by the supplied datastore
-    /// Assumes that the characteristic DECLARATION handles are one less than
-    /// the characteristic handles.
-    /// Returns failure if handles overlap with ones already allocated
+    /// Add a service with pre-allocated handles (for co-existence with C++) backed by the supplied
+    /// datastore Assumes that the characteristic DECLARATION handles are one less than the
+    /// characteristic handles.  Returns failure if handles overlap with ones already allocated.
     pub fn add_service_with_handles(
         &self,
         service: GattServiceWithHandle,
         datastore: Rc<dyn RawGattDatastore>,
     ) -> Result<()> {
         let mut attributes = BTreeMap::new();
-        let mut attribute_cnt = 0;
 
-        let mut add_attribute = |attribute: AttAttribute, value: AttAttributeBackingValue| {
-            attribute_cnt += 1;
-            attributes.insert(attribute.handle, AttAttributeWithBackingValue { attribute, value })
-        };
+        let mut add_attribute =
+            |attribute: AttAttribute, value: AttAttributeBackingValue| match attributes
+                .insert(attribute.handle, AttAttributeWithBackingValue { attribute, value })
+            {
+                None => Ok(()),
+                Some(_) => {
+                    Err(anyhow::anyhow!("duplicate handle detected: {:?}", attribute.handle))
+                }
+            };
 
         let mut characteristics = vec![];
 
@@ -184,7 +196,7 @@
                         anyhow::anyhow!("failed to encode primary service declaration: {e:?}")
                     })?,
             ),
-        );
+        )?;
 
         // characteristics
         for characteristic in service.characteristics {
@@ -224,7 +236,7 @@
                         anyhow::anyhow!("failed to encode characteristic declaration: {e:?}")
                     })?,
                 ),
-            );
+            )?;
 
             // value
             add_attribute(
@@ -234,7 +246,7 @@
                     permissions: characteristic.permissions,
                 },
                 AttAttributeBackingValue::DynamicCharacteristic(datastore.clone()),
-            );
+            )?;
 
             // descriptors
             for descriptor in characteristic.descriptors {
@@ -245,7 +257,7 @@
                         permissions: descriptor.permissions,
                     },
                     AttAttributeBackingValue::DynamicDescriptor(datastore.clone()),
-                );
+                )?;
             }
         }
 
@@ -254,12 +266,9 @@
 
         for handle in attributes.keys() {
             if static_data.attributes.contains_key(handle) {
-                bail!("duplicate handle detected");
+                bail!("handle conflicts with existing service: {handle:?}");
             }
         }
-        if attributes.len() != attribute_cnt {
-            bail!("duplicate handle detected");
-        }
 
         // if we made it here, we successfully loaded the new service
         static_data.attributes.extend(attributes.clone());
@@ -318,171 +327,34 @@
 
         Ok(())
     }
-}
 
-impl SharedBox<GattDatabase> {
-    /// Generate an impl AttDatabase from a backing GattDatabase, associated
-    /// with a given connection.
-    ///
-    /// Note: After the AttDatabaseImpl is constructed, we MUST call on_bearer_ready() with
-    /// the resultant bearer, so that the listeners get the correct sequence of callbacks.
-    pub fn get_att_database(&self, tcb_idx: TransportIndex) -> AttDatabaseImpl {
-        AttDatabaseImpl { gatt_db: self.downgrade(), tcb_idx }
-    }
-}
-
-/// An implementation of AttDatabase wrapping an underlying GattDatabase
-pub struct AttDatabaseImpl {
-    gatt_db: WeakBox<GattDatabase>,
-    tcb_idx: TransportIndex,
-}
-
-#[async_trait(?Send)]
-impl AttDatabase for AttDatabaseImpl {
-    async fn read_attribute(&self, handle: AttHandle) -> Result<Vec<u8>, AttErrorCode> {
-        let value = self.gatt_db.with(|gatt_db| {
-            let Some(gatt_db) = gatt_db else {
-                // db must have been closed
-                return Err(AttErrorCode::InvalidHandle);
-            };
-            let services = gatt_db.schema.borrow();
-            let Some(attr) = services.attributes.get(&handle) else {
-                return Err(AttErrorCode::InvalidHandle);
-            };
-            if !attr.attribute.permissions.readable() {
-                return Err(AttErrorCode::ReadNotPermitted);
-            }
-            Ok(attr.value.clone())
-        })?;
-
-        match value {
-            AttAttributeBackingValue::Static(val) => return Ok(val),
-            AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
-                datastore
-                    .read(
-                        self.tcb_idx,
-                        handle,
-                        /* offset */ 0,
-                        AttributeBackingType::Characteristic,
-                    )
-                    .await
-            }
-            AttAttributeBackingValue::DynamicDescriptor(datastore) => {
-                datastore
-                    .read(
-                        self.tcb_idx,
-                        handle,
-                        /* offset */ 0,
-                        AttributeBackingType::Descriptor,
-                    )
-                    .await
-            }
+    /// Returns a database with the exact characteristics specified. No additional characteristics
+    /// are added for the service or characteristic declarations.
+    #[cfg(test)]
+    pub fn with_characteristics(
+        characteristics: impl IntoIterator<Item = AttAttribute>,
+        datastore: Rc<dyn RawGattDatastore>,
+    ) -> Self {
+        Self {
+            schema: RefCell::new(GattDatabaseSchema {
+                attributes: characteristics
+                    .into_iter()
+                    .map(|attribute| {
+                        (
+                            attribute.handle,
+                            AttAttributeWithBackingValue {
+                                attribute,
+                                value: AttAttributeBackingValue::DynamicCharacteristic(Rc::clone(
+                                    &datastore,
+                                )),
+                            },
+                        )
+                    })
+                    .collect(),
+            }),
+            listeners: RefCell::default(),
         }
     }
-
-    async fn write_attribute(&self, handle: AttHandle, data: &[u8]) -> Result<(), AttErrorCode> {
-        let value = self.gatt_db.with(|gatt_db| {
-            let Some(gatt_db) = gatt_db else {
-                // db must have been closed
-                return Err(AttErrorCode::InvalidHandle);
-            };
-            let services = gatt_db.schema.borrow();
-            let Some(attr) = services.attributes.get(&handle) else {
-                return Err(AttErrorCode::InvalidHandle);
-            };
-            if !attr.attribute.permissions.writable_with_response() {
-                return Err(AttErrorCode::WriteNotPermitted);
-            }
-            Ok(attr.value.clone())
-        })?;
-
-        match value {
-            AttAttributeBackingValue::Static(val) => {
-                error!("A static attribute {val:?} is marked as writable - ignoring it and rejecting the write...");
-                return Err(AttErrorCode::WriteNotPermitted);
-            }
-            AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
-                datastore
-                    .write(
-                        self.tcb_idx,
-                        handle,
-                        AttributeBackingType::Characteristic,
-                        GattWriteRequestType::Request,
-                        data,
-                    )
-                    .await
-            }
-            AttAttributeBackingValue::DynamicDescriptor(datastore) => {
-                datastore
-                    .write(
-                        self.tcb_idx,
-                        handle,
-                        AttributeBackingType::Descriptor,
-                        GattWriteRequestType::Request,
-                        data,
-                    )
-                    .await
-            }
-        }
-    }
-
-    fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]) {
-        let value = self.gatt_db.with(|gatt_db| {
-            let Some(gatt_db) = gatt_db else {
-                // db must have been closed
-                return None;
-            };
-            let services = gatt_db.schema.borrow();
-            let Some(attr) = services.attributes.get(&handle) else {
-                warn!("cannot find handle {handle:?}");
-                return None;
-            };
-            if !attr.attribute.permissions.writable_without_response() {
-                warn!("trying to write without response to {handle:?}, which doesn't support it");
-                return None;
-            }
-            Some(attr.value.clone())
-        });
-
-        let Some(value) = value else {
-            return;
-        };
-
-        match value {
-            AttAttributeBackingValue::Static(val) => {
-                error!("A static attribute {val:?} is marked as writable - ignoring it and rejecting the write...");
-            }
-            AttAttributeBackingValue::DynamicCharacteristic(datastore) => {
-                datastore.write_no_response(
-                    self.tcb_idx,
-                    handle,
-                    AttributeBackingType::Characteristic,
-                    data,
-                );
-            }
-            AttAttributeBackingValue::DynamicDescriptor(datastore) => {
-                datastore.write_no_response(
-                    self.tcb_idx,
-                    handle,
-                    AttributeBackingType::Descriptor,
-                    data,
-                );
-            }
-        };
-    }
-
-    fn list_attributes(&self) -> Vec<AttAttribute> {
-        self.gatt_db.with(|db| {
-            db.map(|db| db.schema.borrow().attributes.values().map(|attr| attr.attribute).collect())
-                .unwrap_or_default()
-        })
-    }
-}
-
-impl Clone for AttDatabaseImpl {
-    fn clone(&self) -> Self {
-        Self { gatt_db: self.gatt_db.clone(), tcb_idx: self.tcb_idx }
-    }
 }
 
 #[cfg(test)]
@@ -491,10 +363,12 @@
     use tokio::sync::mpsc::error::TryRecvError;
     use tokio::task::spawn_local;
 
+    use crate::gatt::ffi::AttributeBackingType;
     use crate::gatt::mocks::mock_database_callbacks::{MockCallbackEvents, MockCallbacks};
     use crate::gatt::mocks::mock_datastore::{MockDatastore, MockDatastoreEvents};
     use crate::gatt::mocks::mock_raw_datastore::{MockRawDatastore, MockRawDatastoreEvents};
-    use crate::packets::att;
+    use crate::gatt::server::att_client::AttClient;
+    use crate::packets::att::{self, AttErrorCode};
     use crate::utils::task::block_on_locally;
 
     use super::*;
@@ -514,9 +388,9 @@
     #[test]
     fn test_read_empty_db() {
         let gatt_db = SharedBox::new(GattDatabase::new());
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
-        let resp = tokio_test::block_on(att_db.read_attribute(AttHandle(1)));
+        let resp = tokio_test::block_on(client.read_attribute(AttHandle(1)));
 
         assert_eq!(resp, Err(AttErrorCode::InvalidHandle))
     }
@@ -535,10 +409,9 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
-
-        let attrs = att_db.list_attributes();
-        let service_value = tokio_test::block_on(att_db.read_attribute(SERVICE_HANDLE));
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+        let attrs = client.list_attributes();
+        let service_value = tokio_test::block_on(client.read_attribute(SERVICE_HANDLE));
 
         assert_eq!(
             attrs,
@@ -608,12 +481,12 @@
                 gatt_datastore,
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
-        assert_eq!(att_db.list_attributes().len(), 9);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+        assert_eq!(client.list_attributes().len(), 9);
 
         // act: remove the middle service
         gatt_db.remove_service_at_handle(AttHandle(4)).unwrap();
-        let attrs = att_db.list_attributes();
+        let attrs = client.list_attributes();
 
         // assert that the middle service is gone
         assert_eq!(attrs.len(), 6, "{attrs:?}");
@@ -658,11 +531,11 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
-        let attrs = att_db.list_attributes();
+        let attrs = client.list_attributes();
         let characteristic_decl =
-            tokio_test::block_on(att_db.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE));
+            tokio_test::block_on(client.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE));
 
         assert_eq!(attrs.len(), 3, "{attrs:?}");
         assert_eq!(attrs[0].type_, PRIMARY_SERVICE_DECLARATION_UUID);
@@ -711,7 +584,7 @@
         // arrange
         let (gatt_datastore, _) = MockDatastore::new();
         let gatt_db = SharedBox::new(GattDatabase::new());
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // act: add a characteristic with all permission bits set
         gatt_db
@@ -732,7 +605,7 @@
 
         // assert: the characteristic declaration has all the bits we support set
         let characteristic_decl =
-            tokio_test::block_on(att_db.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE));
+            tokio_test::block_on(client.read_attribute(CHARACTERISTIC_DECLARATION_HANDLE));
         assert_eq!(
             characteristic_decl,
             att::GattCharacteristicDeclarationValue {
@@ -774,7 +647,7 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: read from the database, and supply a value from the backing datastore
@@ -792,7 +665,7 @@
                     };
                     reply.send(Ok(data.to_vec())).unwrap();
                 },
-                att_db.read_attribute(CHARACTERISTIC_VALUE_HANDLE)
+                client.read_attribute(CHARACTERISTIC_VALUE_HANDLE)
             )
             .1
         });
@@ -821,9 +694,9 @@
             )
             .unwrap();
 
-        let characteristic_value = tokio_test::block_on(
-            gatt_db.get_att_database(TCB_IDX).read_attribute(CHARACTERISTIC_VALUE_HANDLE),
-        );
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+        let characteristic_value =
+            tokio_test::block_on(client.downgrade().read_attribute(CHARACTERISTIC_VALUE_HANDLE));
 
         assert_eq!(characteristic_value, Err(AttErrorCode::ReadNotPermitted));
     }
@@ -899,14 +772,14 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: write to the database
         let recv_data = block_on_locally(async {
             // start write task
             spawn_local(async move {
-                att_db.write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data).await.unwrap();
+                client.write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data).await.unwrap();
             });
 
             let MockDatastoreEvents::Write(
@@ -946,7 +819,7 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: write to the database
@@ -960,7 +833,7 @@
                     };
                     reply.send(Err(AttErrorCode::UnlikelyError)).unwrap();
                 },
-                att_db.write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data)
+                client.write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data)
             )
             .1
         });
@@ -990,8 +863,9 @@
             .unwrap();
         let data = [1, 2];
 
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let characteristic_value = tokio_test::block_on(
-            gatt_db.get_att_database(TCB_IDX).write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data),
+            client.downgrade().write_attribute(CHARACTERISTIC_VALUE_HANDLE, &data),
         );
 
         assert_eq!(characteristic_value, Err(AttErrorCode::WriteNotPermitted));
@@ -1020,13 +894,13 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         let descriptor_value = block_on_locally(async {
             // start write task
             let pending_read =
-                spawn_local(async move { att_db.read_attribute(DESCRIPTOR_HANDLE).await.unwrap() });
+                spawn_local(async move { client.read_attribute(DESCRIPTOR_HANDLE).await.unwrap() });
 
             let MockDatastoreEvents::Read(
                 TCB_IDX,
@@ -1070,14 +944,14 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: write, and wait for the callback to be invoked
         block_on_locally(async {
             // start write task
             spawn_local(
-                async move { att_db.write_attribute(DESCRIPTOR_HANDLE, &data).await.unwrap() },
+                async move { client.write_attribute(DESCRIPTOR_HANDLE, &data).await.unwrap() },
             );
 
             let MockDatastoreEvents::Write(
@@ -1141,7 +1015,8 @@
             .unwrap();
 
         // act: get the attributes
-        let attributes = gatt_db.get_att_database(TCB_IDX).list_attributes();
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+        let attributes = client.downgrade().list_attributes();
 
         // assert: check the attributes are in the correct order
         assert_eq!(attributes.len(), 8);
@@ -1205,7 +1080,7 @@
             )
             .unwrap();
 
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: read from the second characteristic and supply a response from the second datastore
@@ -1223,7 +1098,7 @@
                     };
                     reply.send(Ok(data.to_vec())).unwrap();
                 },
-                att_db.read_attribute(AttHandle(6))
+                client.read_attribute(AttHandle(6))
             )
             .1
         });
@@ -1236,28 +1111,19 @@
         assert_eq!(data_evts_2.try_recv().unwrap_err(), TryRecvError::Empty);
     }
 
-    fn make_bearer(
-        gatt_db: &SharedBox<GattDatabase>,
-    ) -> SharedBox<AttServerBearer<AttDatabaseImpl>> {
-        SharedBox::new(AttServerBearer::new(gatt_db.get_att_database(TCB_IDX), |_| {
-            unreachable!();
-        }))
-    }
-
     #[test]
     fn test_connection_listener() {
         // arrange: db with a listener
         let gatt_db = SharedBox::new(GattDatabase::new());
         let (callbacks, mut rx) = MockCallbacks::new();
         gatt_db.register_listener(Rc::new(callbacks));
-        let bearer = make_bearer(&gatt_db);
 
         // act: open a connection
-        gatt_db.on_bearer_ready(TCB_IDX, &bearer);
+        let _client = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // assert: we got the callback
         let event = rx.blocking_recv().unwrap();
-        assert!(matches!(event, MockCallbackEvents::OnLeConnect(TCB_IDX, _)));
+        assert!(matches!(event, MockCallbackEvents::OnLeConnect(_)));
     }
 
     #[test]
@@ -1265,10 +1131,11 @@
         // arrange: db with a listener
         let gatt_db = SharedBox::new(GattDatabase::new());
         let (callbacks, mut rx) = MockCallbacks::new();
+        let client = AttClient::new_test_client(TCB_IDX, &gatt_db);
         gatt_db.register_listener(Rc::new(callbacks));
 
         // act: disconnect
-        gatt_db.on_bearer_dropped(TCB_IDX);
+        std::mem::drop(client);
 
         // assert: we got the callback
         let event = rx.blocking_recv().unwrap();
@@ -1279,13 +1146,15 @@
     fn test_multiple_listeners() {
         // arrange: db with two listeners
         let gatt_db = SharedBox::new(GattDatabase::new());
+        let client = AttClient::new_test_client(TCB_IDX, &gatt_db);
+
         let (callbacks1, mut rx1) = MockCallbacks::new();
         gatt_db.register_listener(Rc::new(callbacks1));
         let (callbacks2, mut rx2) = MockCallbacks::new();
         gatt_db.register_listener(Rc::new(callbacks2));
 
         // act: disconnect
-        gatt_db.on_bearer_dropped(TCB_IDX);
+        std::mem::drop(client);
 
         // assert: we got the callback on both listeners
         let event = rx1.blocking_recv().unwrap();
@@ -1464,11 +1333,11 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: write without response to the database
-        att_db.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, &data);
+        client.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, &data);
 
         // assert: we got a callback
         let event = data_evts.blocking_recv().unwrap();
@@ -1505,11 +1374,11 @@
                 Rc::new(gatt_datastore),
             )
             .unwrap();
-        let att_db = gatt_db.get_att_database(TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
         let data = [1, 2];
 
         // act: try writing without response to this characteristic
-        att_db.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, &data);
+        client.write_no_response_attribute(CHARACTERISTIC_VALUE_HANDLE, &data);
 
         // assert: no callback was sent
         assert_eq!(data_events.try_recv().unwrap_err(), TryRecvError::Empty);
diff --git a/system/rust/src/gatt/server/indication_handler.rs b/system/rust/src/gatt/server/indication_handler.rs
index 501f580..12c0239 100644
--- a/system/rust/src/gatt/server/indication_handler.rs
+++ b/system/rust/src/gatt/server/indication_handler.rs
@@ -6,9 +6,9 @@
 use tokio::time::timeout;
 
 use crate::gatt::ids::AttHandle;
-use crate::packets::att;
+use crate::gatt::server::att_client::WeakAttClient;
+use crate::packets::att::{self, AttErrorCode};
 
-use super::att_database::{AttDatabase, StableAttDatabase};
 use super::att_server_bearer::SendError;
 
 #[derive(Debug)]
@@ -34,15 +34,21 @@
     ConnectionDroppedWhileWaitingForConfirmation,
 }
 
-pub struct IndicationHandler<T> {
-    db: T,
+impl From<AttErrorCode> for IndicationError {
+    fn from(_value: AttErrorCode) -> Self {
+        IndicationError::AttributeNotFound
+    }
+}
+
+pub struct IndicationHandler {
+    client: WeakAttClient,
     pending_confirmation: mpsc::Receiver<()>,
 }
 
-impl<T: AttDatabase> IndicationHandler<T> {
-    pub fn new(db: T) -> (Self, ConfirmationWatcher) {
+impl IndicationHandler {
+    pub fn new(client: WeakAttClient) -> (Self, ConfirmationWatcher) {
         let (tx, rx) = mpsc::channel(1);
-        (Self { db, pending_confirmation: rx }, ConfirmationWatcher(tx))
+        (Self { client, pending_confirmation: rx }, ConfirmationWatcher(tx))
     }
 
     pub async fn send(
@@ -50,7 +56,6 @@
         handle: AttHandle,
         data: &[u8],
         mtu: usize,
-        send_packet: impl FnOnce(att::Att) -> Result<(), SendError>,
     ) -> Result<(), IndicationError> {
         let data_size = data.len();
         // As per Core Spec 5.3 Vol 3F 3.4.7.2, the indicated value must be at most
@@ -59,27 +64,26 @@
             return Err(IndicationError::DataExceedsMtu { mtu: mtu - 3 });
         }
 
-        if !self
-            .db
-            .snapshot()
-            .find_attribute(handle)
-            .ok_or(IndicationError::AttributeNotFound)?
-            .permissions
-            .indicate()
-        {
-            warn!("cannot send indication for {handle:?} since it does not support indications");
-            return Err(IndicationError::IndicationsNotSupported);
-        }
+        self.client.with_attribute(handle, |client, attr| {
+            if !attr.attribute.permissions.indicate() {
+                warn!(
+                    "cannot send indication for {handle:?} since it does not support indications"
+                );
+                return Err(IndicationError::IndicationsNotSupported);
+            }
 
-        // flushing any confirmations that arrived before we sent the next indication
-        let _ = self.pending_confirmation.try_recv();
+            // flushing any confirmations that arrived before we sent the next indication
+            let _ = self.pending_confirmation.try_recv();
 
-        send_packet(
-            att::AttHandleValueIndication { handle: handle.into(), value: data.to_vec() }
-                .try_into()
-                .unwrap(),
-        )
-        .map_err(IndicationError::SendError)?;
+            client
+                .bearer()
+                .send_packet(
+                    att::AttHandleValueIndication { handle: handle.into(), value: data.to_vec() }
+                        .try_into()
+                        .unwrap(),
+                )
+                .map_err(|e| IndicationError::SendError(SendError::SerializeError(e)))
+        })?;
 
         match timeout(Duration::from_secs(30), self.pending_confirmation.recv()).await {
             Ok(Some(())) => Ok(()),
@@ -116,26 +120,33 @@
 #[cfg(test)]
 mod test {
     use crate::packets::att;
-    use tokio::sync::oneshot;
     use tokio::task::spawn_local;
     use tokio::time::Instant;
 
+    use crate::core::shared_box::SharedBox;
     use crate::core::uuid::Uuid;
+    use crate::gatt::ids::TransportIndex;
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::AttAttribute;
-    use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::gatt_database::{AttPermissions, GattDatabase};
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::utils::task::block_on_locally;
 
     use super::*;
 
-    const HANDLE: AttHandle = AttHandle(1);
-    const NONEXISTENT_HANDLE: AttHandle = AttHandle(2);
-    const NON_INDICATE_HANDLE: AttHandle = AttHandle(3);
+    const HANDLE: AttHandle = AttHandle(3);
+    const NONEXISTENT_HANDLE: AttHandle = AttHandle(4);
+    const NON_INDICATE_HANDLE: AttHandle = AttHandle(6);
     const MTU: usize = 32;
     const DATA: [u8; 3] = [1, 2, 3];
+    const TCB_IDX: TransportIndex = TransportIndex(1);
 
-    fn get_att_database() -> TestAttDatabase {
-        TestAttDatabase::new(vec![
+    fn set_up() -> (
+        SharedBox<GattDatabase>,
+        SharedBox<AttClient>,
+        tokio::sync::mpsc::UnboundedReceiver<att::Att>,
+    ) {
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: HANDLE,
@@ -152,29 +163,24 @@
                 },
                 vec![],
             ),
-        ])
+        ]);
+        let (client, rx) = AttClient::new_test_client(TCB_IDX, &db);
+        (db, client, rx)
     }
 
     #[test]
     fn test_indication_sent() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, mut rx) = set_up();
             let (mut indication_handler, _confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
-            let (tx, rx) = oneshot::channel();
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication
-            spawn_local(async move {
-                indication_handler
-                    .send(HANDLE, &DATA, MTU, move |packet| {
-                        tx.send(packet).unwrap();
-                        Ok(())
-                    })
-                    .await
-            });
+            spawn_local(async move { indication_handler.send(HANDLE, &DATA, MTU).await });
 
             // assert: that an AttHandleValueIndication was sent on the channel
-            let indication = rx.await.unwrap();
+            let indication = rx.recv().await.unwrap();
             assert_eq!(
                 Ok(indication),
                 att::AttHandleValueIndication { handle: HANDLE.into(), value: DATA.to_vec() }
@@ -187,13 +193,12 @@
     fn test_invalid_handle() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, _rx) = set_up();
             let (mut indication_handler, _confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication on a nonexistent handle
-            let ret = indication_handler
-                .send(NONEXISTENT_HANDLE, &DATA, MTU, move |_| unreachable!())
-                .await;
+            let ret = indication_handler.send(NONEXISTENT_HANDLE, &DATA, MTU).await;
 
             // assert: that we failed with IndicationError::AttributeNotFound
             assert!(matches!(ret, Err(IndicationError::AttributeNotFound)));
@@ -204,13 +209,12 @@
     fn test_unsupported_permission() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, _rx) = set_up();
             let (mut indication_handler, _confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication on an attribute that does not support indications
-            let ret = indication_handler
-                .send(NON_INDICATE_HANDLE, &DATA, MTU, move |_| unreachable!())
-                .await;
+            let ret = indication_handler.send(NON_INDICATE_HANDLE, &DATA, MTU).await;
 
             // assert: that we failed with IndicationError::IndicationsNotSupported
             assert!(matches!(ret, Err(IndicationError::IndicationsNotSupported)));
@@ -221,21 +225,15 @@
     fn test_confirmation_handled() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, mut rx) = set_up();
             let (mut indication_handler, confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
-            let (tx, rx) = oneshot::channel();
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication
-            let pending_result = spawn_local(async move {
-                indication_handler
-                    .send(HANDLE, &DATA, MTU, move |packet| {
-                        tx.send(packet).unwrap();
-                        Ok(())
-                    })
-                    .await
-            });
+            let pending_result =
+                spawn_local(async move { indication_handler.send(HANDLE, &DATA, MTU).await });
             // when the indication is sent, send a confirmation in response
-            rx.await.unwrap();
+            rx.recv().await.unwrap();
             confirmation_watcher.on_confirmation();
 
             // assert: the indication was successfully sent
@@ -247,22 +245,16 @@
     fn test_unblock_on_disconnect() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, mut rx) = set_up();
             let (mut indication_handler, confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
-            let (tx, rx) = oneshot::channel();
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication
-            let pending_result = spawn_local(async move {
-                indication_handler
-                    .send(HANDLE, &DATA, MTU, move |packet| {
-                        tx.send(packet).unwrap();
-                        Ok(())
-                    })
-                    .await
-            });
+            let pending_result =
+                spawn_local(async move { indication_handler.send(HANDLE, &DATA, MTU).await });
             // when the indication is sent, drop the confirmation watcher (as would happen
             // upon a disconnection)
-            rx.await.unwrap();
+            rx.recv().await.unwrap();
             drop(confirmation_watcher);
 
             // assert: we get the appropriate error
@@ -277,24 +269,18 @@
     fn test_spurious_confirmations() {
         block_on_locally(async move {
             // arrange: send a few confirmations in advance
+            let (_db, client, mut rx) = set_up();
             let (mut indication_handler, confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
-            let (tx, rx) = oneshot::channel();
+                IndicationHandler::new(client.downgrade());
             confirmation_watcher.on_confirmation();
             confirmation_watcher.on_confirmation();
 
             // act: send an indication
-            let pending_result = spawn_local(async move {
-                indication_handler
-                    .send(HANDLE, &DATA, MTU, move |packet| {
-                        tx.send(packet).unwrap();
-                        Ok(())
-                    })
-                    .await
-            });
+            let pending_result =
+                spawn_local(async move { indication_handler.send(HANDLE, &DATA, MTU).await });
             // when the indication is sent, drop the confirmation watcher (so we won't block
             // forever)
-            rx.await.unwrap();
+            rx.recv().await.unwrap();
             drop(confirmation_watcher);
 
             // assert: we get the appropriate error, rather than an Ok(())
@@ -311,24 +297,18 @@
     fn test_indication_timeout() {
         block_on_locally(async move {
             // arrange: send a few confirmations in advance
+            let (_db, client, mut rx) = set_up();
             let (mut indication_handler, confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
-            let (tx, rx) = oneshot::channel();
+                IndicationHandler::new(client.downgrade());
             confirmation_watcher.on_confirmation();
             confirmation_watcher.on_confirmation();
 
             // act: send an indication
             let time_sent = Instant::now();
-            let pending_result = spawn_local(async move {
-                indication_handler
-                    .send(HANDLE, &DATA, MTU, move |packet| {
-                        tx.send(packet).unwrap();
-                        Ok(())
-                    })
-                    .await
-            });
+            let pending_result =
+                spawn_local(async move { indication_handler.send(HANDLE, &DATA, MTU).await });
             // after it is sent, wait for the timer to fire
-            rx.await.unwrap();
+            rx.recv().await.unwrap();
 
             // assert: we get the appropriate error
             assert!(matches!(
@@ -348,11 +328,12 @@
     fn test_mtu_exceeds() {
         block_on_locally(async move {
             // arrange
+            let (_db, client, _rx) = set_up();
             let (mut indication_handler, _confirmation_watcher) =
-                IndicationHandler::new(get_att_database());
+                IndicationHandler::new(client.downgrade());
 
             // act: send an indication with an ATT_MTU of 4 and data length of 3
-            let res = indication_handler.send(HANDLE, &DATA, 4, move |_| unreachable!()).await;
+            let res = indication_handler.send(HANDLE, &DATA, 4).await;
 
             // assert: that we got the expected error, indicating the max data size (not the
             // ATT_MTU, but ATT_MTU-3)
diff --git a/system/rust/src/gatt/server/request_handler.rs b/system/rust/src/gatt/server/request_handler.rs
index 76832bb..7375ed7 100644
--- a/system/rust/src/gatt/server/request_handler.rs
+++ b/system/rust/src/gatt/server/request_handler.rs
@@ -3,9 +3,9 @@
 
 use crate::gatt::ids::AttHandle;
 use crate::gatt::opcode_types::AttRequest;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att::{self, AttErrorCode};
 
-use super::att_database::AttDatabase;
 use super::transactions::find_by_type_value::handle_find_by_type_value_request;
 use super::transactions::find_information_request::handle_find_information_request;
 use super::transactions::read_by_group_type_request::handle_read_by_group_type_request;
@@ -13,12 +13,14 @@
 use super::transactions::read_request::{
     handle_read_blob_request, handle_read_multiple_variable_request, handle_read_request,
 };
-use super::transactions::write_request::handle_write_request;
+use super::transactions::write_request::{
+    handle_execute_write_request, handle_prepare_write_request, handle_write_request,
+};
 
 /// This struct handles all requests needing ACKs. Only ONE should exist per
 /// bearer per database, to ensure serialization.
-pub struct AttRequestHandler<Db: AttDatabase> {
-    db: Db,
+pub struct AttRequestHandler {
+    client: WeakAttClient,
 }
 
 /// Type of errors raised by request handlers.
@@ -41,9 +43,9 @@
     }
 }
 
-impl<Db: AttDatabase> AttRequestHandler<Db> {
-    pub fn new(db: Db) -> Self {
-        Self { db }
+impl AttRequestHandler {
+    pub fn new(client: WeakAttClient) -> Self {
+        Self { client }
     }
 
     // Runs a task to process an incoming *request* packet. There should be only one instance of
@@ -71,34 +73,38 @@
         packet: &AttRequest,
         mtu: usize,
     ) -> Result<att::Att, ProcessingError> {
-        let snapshotted_db = self.db.snapshot();
         let packet = &**packet;
         match packet.opcode {
             att::AttOpcode::ReadRequest => {
-                Ok(handle_read_request(packet.try_into()?, mtu, &self.db).await?)
+                Ok(handle_read_request(packet.try_into()?, mtu, &self.client).await?)
             }
             att::AttOpcode::ReadBlobRequest => {
-                Ok(handle_read_blob_request(packet.try_into()?, mtu, &self.db).await?)
+                Ok(handle_read_blob_request(packet.try_into()?, mtu, &self.client).await?)
             }
             att::AttOpcode::ReadMultipleVariableRequest => {
-                Ok(handle_read_multiple_variable_request(packet.try_into()?, mtu, &self.db).await?)
+                Ok(handle_read_multiple_variable_request(packet.try_into()?, mtu, &self.client)
+                    .await?)
             }
             att::AttOpcode::ReadByGroupTypeRequest => {
-                Ok(handle_read_by_group_type_request(packet.try_into()?, mtu, &snapshotted_db)
-                    .await?)
+                Ok(handle_read_by_group_type_request(packet.try_into()?, mtu, &self.client).await?)
             }
             att::AttOpcode::ReadByTypeRequest => {
-                Ok(handle_read_by_type_request(packet.try_into()?, mtu, &snapshotted_db).await?)
+                Ok(handle_read_by_type_request(packet.try_into()?, mtu, &self.client).await?)
             }
             att::AttOpcode::FindInformationRequest => {
-                Ok(handle_find_information_request(packet.try_into()?, mtu, &snapshotted_db)?)
+                Ok(handle_find_information_request(packet.try_into()?, mtu, &self.client)?)
             }
             att::AttOpcode::FindByTypeValueRequest => {
-                Ok(handle_find_by_type_value_request(packet.try_into()?, mtu, &snapshotted_db)
-                    .await?)
+                Ok(handle_find_by_type_value_request(packet.try_into()?, mtu, &self.client).await?)
             }
             att::AttOpcode::WriteRequest => {
-                Ok(handle_write_request(packet.try_into()?, &self.db).await?)
+                Ok(handle_write_request(packet.try_into()?, &self.client).await?)
+            }
+            att::AttOpcode::PrepareWriteRequest => {
+                Ok(handle_prepare_write_request(packet.try_into()?, &self.client).await?)
+            }
+            att::AttOpcode::ExecuteWriteRequest => {
+                Ok(handle_execute_write_request(packet.try_into()?, &self.client).await?)
             }
             _ => {
                 warn!("Dropping unsupported opcode {:?}", packet.opcode);
@@ -112,17 +118,25 @@
 mod test {
     use super::*;
 
+    use crate::core::shared_box::SharedBox;
     use crate::core::uuid::Uuid;
+    use crate::gatt::ids::TransportIndex;
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::{AttAttribute, AttPermissions};
+    use crate::gatt::server::gatt_database::{
+        GattCharacteristicWithHandle, GattDatabase, GattServiceWithHandle,
+    };
     use crate::gatt::server::request_handler::AttRequestHandler;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::{new_test_database, TestDatastore};
     use crate::packets::att;
     use pdl_runtime::Packet;
 
+    const TCB_IDX: TransportIndex = TransportIndex(1);
+
     #[test]
     fn test_read_request() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -130,7 +144,8 @@
             },
             vec![1, 2, 3],
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
         let att_view =
             AttRequest::new(att::AttReadRequest { attribute_handle: AttHandle(3).into() }).unwrap();
 
@@ -145,7 +160,7 @@
     fn test_read_blob_request() {
         // arrange
         let data: Vec<u8> = (0..255).collect();
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -153,7 +168,8 @@
             },
             data.clone(),
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
         const MTU: usize = 31;
 
         // Returns the expected part of `data` for the `offset`.
@@ -182,7 +198,7 @@
     fn test_read_blob_request_with_bad_offset() {
         // arrange
         let data: Vec<u8> = (0..255).collect();
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -190,7 +206,8 @@
             },
             data.clone(),
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadBlobRequest {
             attribute_handle: AttHandle(3).into(),
@@ -217,7 +234,7 @@
     fn test_read_blob_request_with_invalid_handle() {
         // arrange
         let data: Vec<u8> = (0..255).collect();
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -225,7 +242,8 @@
             },
             data.clone(),
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadBlobRequest {
             attribute_handle: AttHandle(4).into(),
@@ -251,7 +269,7 @@
     #[test]
     fn test_read_multiple_variable_request() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -269,7 +287,8 @@
                 vec![b'4'],
             ),
         ]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into(), AttHandle(4).into()].into(),
@@ -297,7 +316,7 @@
     fn test_read_multiple_variable_request_truncated() {
         let data: Vec<u8> = (0..255).collect();
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -323,7 +342,8 @@
                 vec![b'5'],
             ),
         ]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into(), AttHandle(4).into(), AttHandle(5).into()]
@@ -361,7 +381,7 @@
         let data = vec![0xaf; MTU - 1 - 3 - 2 - 1];
 
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -387,7 +407,8 @@
                 vec![b'5'],
             ),
         ]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into(), AttHandle(4).into(), AttHandle(5).into()]
@@ -417,7 +438,7 @@
     #[test]
     fn test_read_multiple_variable_request_at_least_two() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -425,7 +446,8 @@
             },
             vec![b'3'],
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into()].into(),
@@ -450,7 +472,7 @@
     #[test]
     fn test_read_multiple_variable_request_all_handles_valid() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x1234),
@@ -458,7 +480,8 @@
             },
             vec![0xaf; 255],
         )]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into(), AttHandle(5).into()].into(),
@@ -483,7 +506,7 @@
     #[test]
     fn test_read_multiple_variable_request_value_longer_than_65535_bytes() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -501,7 +524,8 @@
                 vec![0xaf; 65536],
             ),
         ]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
 
         let att_view = AttRequest::new(att::AttReadMultipleVariableRequest {
             attribute_handles: [AttHandle(3).into(), AttHandle(4).into()].into(),
@@ -524,9 +548,457 @@
     }
 
     #[test]
+    fn test_queued_writes() {
+        // arrange
+        let data = vec![b'3'; 100];
+        let db = new_test_database(vec![(
+            AttAttribute {
+                handle: AttHandle(3),
+                type_: Uuid::new(0x1234),
+                permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+            },
+            data.clone(),
+        )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
+
+        // act & assert
+        tokio_test::block_on(async {
+            let mut expected = data.clone();
+
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 5,
+                value: vec![b'4'; 10],
+            };
+            expected[5..15].fill(b'4');
+
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Reading the attribute should return the original value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest {
+                            attribute_handle: AttHandle(3).into()
+                        })
+                        .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: data.clone() }.try_into()
+            );
+
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 30,
+                value: vec![b'5'; 7],
+            };
+            expected[30..37].fill(b'5');
+
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Reading the attribute should return the original value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest {
+                            attribute_handle: AttHandle(3).into()
+                        })
+                        .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: data.clone() }.try_into()
+            );
+
+            // Execute the queued requests.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 1 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttExecuteWriteResponse {}.try_into()
+            );
+
+            // Reading the attribute should return the new value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest {
+                            attribute_handle: AttHandle(3).into()
+                        })
+                        .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: expected }.try_into()
+            );
+        });
+    }
+
+    #[test]
+    fn test_cancelling_queued_writes() {
+        // arrange
+        let data = vec![b'3'; 100];
+        let db = new_test_database(vec![(
+            AttAttribute {
+                handle: AttHandle(3),
+                type_: Uuid::new(0x1234),
+                permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+            },
+            data.clone(),
+        )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
+
+        // act & assert
+        tokio_test::block_on(async {
+            let mut expected = data.clone();
+
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 5,
+                value: vec![b'4'; 10],
+            };
+
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Cancel the queued request.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 0 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttExecuteWriteResponse {}.try_into()
+            );
+
+            // Prepare and execute another request and it shouldn't have the canceled request.
+
+            // Reading the attribute should return the original value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest {
+                            attribute_handle: AttHandle(3).into()
+                        })
+                        .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: data.clone() }.try_into()
+            );
+
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 30,
+                value: vec![b'5'; 7],
+            };
+            expected[30..37].fill(b'5');
+
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Execute the queued requests.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 1 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttExecuteWriteResponse {}.try_into()
+            );
+
+            // Reading the attribute should return the new value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest {
+                            attribute_handle: AttHandle(3).into()
+                        })
+                        .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: expected }.try_into()
+            );
+        });
+    }
+
+    #[test]
+    fn test_queued_write_with_invalid_offset() {
+        // arrange
+        let data = vec![b'3'; 100];
+        let db = new_test_database(vec![(
+            AttAttribute {
+                handle: AttHandle(3),
+                type_: Uuid::new(0x1234),
+                permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+            },
+            data.clone(),
+        )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
+
+        // act & assert
+        tokio_test::block_on(async {
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 110,
+                value: vec![b'4'; 10],
+            };
+
+            // The prepare should succeed.
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Execute the queued requests.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 1 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttErrorResponse {
+                    opcode_in_error: att::AttOpcode::ExecuteWriteRequest,
+                    handle_in_error: AttHandle(0).into(), // Ideally this would be 3.
+                    error_code: AttErrorCode::InvalidOffset
+                }
+                .try_into()
+            );
+        });
+    }
+
+    #[test]
+    fn test_queued_write_with_invalid_length() {
+        // arrange
+        let data = vec![b'3'; 100];
+        let db = new_test_database(vec![(
+            AttAttribute {
+                handle: AttHandle(3),
+                type_: Uuid::new(0x1234),
+                permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+            },
+            data.clone(),
+        )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
+
+        // act & assert
+        tokio_test::block_on(async {
+            let request = att::AttPrepareWriteRequest {
+                handle: AttHandle(3).into(),
+                offset: 90,
+                value: vec![b'4'; 30],
+            };
+
+            // The prepare should succeed.
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // Execute the queued requests.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 1 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttErrorResponse {
+                    opcode_in_error: att::AttOpcode::ExecuteWriteRequest,
+                    handle_in_error: AttHandle(0).into(), // Ideally this would be 3.
+                    error_code: AttErrorCode::InvalidAttributeValueLength,
+                }
+                .try_into()
+            );
+        });
+    }
+
+    #[test]
+    fn test_queued_write_to_different_datastores() {
+        // arrange
+        let handle1 = AttHandle(5);
+        let handle1_value = [5];
+        let datastore1 = TestDatastore::new([(handle1, handle1_value.into())]);
+        let handle2 = AttHandle(8);
+        let handle2_value = [8];
+        let datastore2 = TestDatastore::new([(handle2, handle2_value.into())]);
+
+        let db = SharedBox::new(GattDatabase::new());
+
+        db.add_service_with_handles(
+            GattServiceWithHandle {
+                handle: AttHandle(3),
+                type_: Uuid::new(1),
+                characteristics: vec![GattCharacteristicWithHandle {
+                    handle: handle1,
+                    type_: Uuid::new(0x1234),
+                    permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+                    descriptors: vec![],
+                }],
+            },
+            datastore1,
+        )
+        .unwrap();
+        db.add_service_with_handles(
+            GattServiceWithHandle {
+                handle: AttHandle(6),
+                type_: Uuid::new(1),
+                characteristics: vec![GattCharacteristicWithHandle {
+                    handle: handle2,
+                    type_: Uuid::new(0x1234),
+                    permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
+                    descriptors: vec![],
+                }],
+            },
+            datastore2,
+        )
+        .unwrap();
+
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
+
+        // act & assert
+        tokio_test::block_on(async {
+            let mut expected = handle1_value.to_vec();
+            let request =
+                att::AttPrepareWriteRequest { handle: handle1.into(), offset: 0, value: vec![15] };
+            expected[0] = 15;
+
+            // The prepare should succeed.
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into()
+            );
+
+            // A prepare to a different datastore should fail.
+            let request =
+                att::AttPrepareWriteRequest { handle: handle2.into(), offset: 0, value: vec![6] };
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request.clone()).unwrap(), 255).await),
+                att::AttErrorResponse {
+                    opcode_in_error: att::AttOpcode::PrepareWriteRequest,
+                    handle_in_error: handle2.into(),
+                    error_code: AttErrorCode::RequestNotSupported,
+                }
+                .try_into()
+            );
+
+            // Executing the existing queued request should still succeed.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttExecuteWriteRequest { commit: 1 }).unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttExecuteWriteResponse {}.try_into()
+            );
+
+            // Reading the attribute should return the new value.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest { attribute_handle: handle1.into() })
+                            .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: expected }.try_into()
+            );
+
+            // Reading attribute 4 should be unchanged.
+            assert_eq!(
+                Ok(handler
+                    .process_packet(
+                        AttRequest::new(att::AttReadRequest { attribute_handle: handle2.into() })
+                            .unwrap(),
+                        255
+                    )
+                    .await),
+                att::AttReadResponse { value: handle2_value.into() }.try_into()
+            );
+
+            // A prepare to a different datastore should now succeed.
+            let expected_response = att::AttPrepareWriteResponse {
+                handle: request.handle.clone(),
+                offset: request.offset,
+                value: request.value.clone(),
+            };
+            assert_eq!(
+                Ok(handler.process_packet(AttRequest::new(request).unwrap(), 255).await),
+                expected_response.try_into(),
+            );
+        });
+    }
+
+    #[test]
     fn test_unsupported_request() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -544,7 +1016,8 @@
                 vec![1, 2, 3],
             ),
         ]);
-        let mut handler = AttRequestHandler { db };
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+        let mut handler = AttRequestHandler::new(client.downgrade());
         let att_view = AttRequest::new(att::AttReadMultipleRequest {
             attribute_handles: vec![AttHandle(3).into(), AttHandle(4).into()],
         })
diff --git a/system/rust/src/gatt/server/services/gap.rs b/system/rust/src/gatt/server/services/gap.rs
index 6402375..5ef3621 100644
--- a/system/rust/src/gatt/server/services/gap.rs
+++ b/system/rust/src/gatt/server/services/gap.rs
@@ -92,20 +92,21 @@
     use super::*;
 
     use crate::core::shared_box::SharedBox;
-    use crate::gatt::server::att_database::AttDatabase;
     use crate::gatt::server::gatt_database::{
         GattDatabase, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID,
     };
+    use crate::gatt::server::AttClient;
     use crate::utils::task::block_on_locally;
 
     const TCB_IDX: TransportIndex = TransportIndex(1);
 
-    fn init_dbs() -> (SharedBox<GattDatabase>, impl AttDatabase) {
+    fn init_dbs() -> (SharedBox<GattDatabase>, SharedBox<AttClient>) {
         let mut gatt_database = GattDatabase::new();
         register_gap_service(&mut gatt_database).unwrap();
         let gatt_database = SharedBox::new(gatt_database);
-        let att_database = gatt_database.get_att_database(TCB_IDX);
-        (gatt_database, att_database)
+        let att_client =
+            AttClient::new_client_and_bearer(TCB_IDX, |_| unreachable!(), &gatt_database);
+        (gatt_database, att_client)
     }
 
     #[test]
@@ -136,10 +137,10 @@
     #[test]
     fn test_read_device_name_not_discoverable() {
         // arrange
-        let (_gatt_db, att_db) = init_dbs();
+        let (_gatt_db, client) = init_dbs();
 
         // act: try to read the device name
-        let name = block_on_locally(att_db.read_attribute(DEVICE_NAME_HANDLE));
+        let name = block_on_locally(client.read_attribute(DEVICE_NAME_HANDLE));
 
         // assert: the name is not readable
         assert_eq!(name, Err(AttErrorCode::InsufficientAuthentication));
@@ -148,10 +149,10 @@
     #[test]
     fn test_read_device_appearance() {
         // arrange
-        let (_gatt_db, att_db) = init_dbs();
+        let (_gatt_db, client) = init_dbs();
 
         // act: try to read the device name
-        let name = block_on_locally(att_db.read_attribute(DEVICE_APPEARANCE_HANDLE));
+        let name = block_on_locally(client.read_attribute(DEVICE_APPEARANCE_HANDLE));
 
         // assert: the name is not readable
         assert_eq!(name, Ok(vec![0x00, 0x00]));
diff --git a/system/rust/src/gatt/server/services/gatt.rs b/system/rust/src/gatt/server/services/gatt.rs
index 11417b0..d7bff05 100644
--- a/system/rust/src/gatt/server/services/gatt.rs
+++ b/system/rust/src/gatt/server/services/gatt.rs
@@ -11,15 +11,15 @@
 use log::{error, warn};
 use tokio::task::spawn_local;
 
-use crate::core::shared_box::{SharedBox, WeakBox};
+use crate::core::shared_box::SharedBox;
 use crate::core::uuid::Uuid;
 use crate::gatt::callbacks::GattDatastore;
 use crate::gatt::ffi::AttributeBackingType;
 use crate::gatt::ids::{AttHandle, TransportIndex};
-use crate::gatt::server::att_server_bearer::AttServerBearer;
+use crate::gatt::server::att_client::{AttClient, WeakAttClient};
 use crate::gatt::server::gatt_database::{
-    AttDatabaseImpl, AttPermissions, GattCharacteristicWithHandle, GattDatabase,
-    GattDatabaseCallbacks, GattDescriptorWithHandle, GattServiceWithHandle,
+    AttPermissions, GattCharacteristicWithHandle, GattDatabase, GattDatabaseCallbacks,
+    GattDescriptorWithHandle, GattServiceWithHandle,
 };
 use crate::packets::att::{self, AttErrorCode};
 
@@ -30,7 +30,7 @@
 
 #[derive(Clone)]
 struct ClientState {
-    bearer: WeakBox<AttServerBearer<AttDatabaseImpl>>,
+    client: WeakAttClient,
     registered_for_service_change: bool,
 }
 
@@ -100,15 +100,11 @@
 }
 
 impl GattDatabaseCallbacks for GattService {
-    fn on_le_connect(
-        &self,
-        tcb_idx: TransportIndex,
-        bearer: &SharedBox<AttServerBearer<AttDatabaseImpl>>,
-    ) {
+    fn on_le_connect(&self, client: &SharedBox<AttClient>) {
         // TODO(aryarahul): registered_for_service_change may not be false for bonded devices
         self.clients.borrow_mut().insert(
-            tcb_idx,
-            ClientState { bearer: bearer.downgrade(), registered_for_service_change: false },
+            client.tcb_idx(),
+            ClientState { client: client.downgrade(), registered_for_service_change: false },
         );
     }
 
@@ -119,10 +115,10 @@
     fn on_service_change(&self, range: RangeInclusive<AttHandle>) {
         for (conn_id, client) in self.clients.borrow().clone() {
             if client.registered_for_service_change {
-                client.bearer.with(|bearer| match bearer {
-                    Some(bearer) => {
+                client.client.with(|client| match client {
+                    Some(client) => {
                         spawn_local(
-                            bearer.send_indication(
+                            client.bearer().send_indication(
                                 SERVICE_CHANGE_HANDLE,
                                 att::GattServiceChanged {
                                     start_handle: (*range.start()).into(),
@@ -134,7 +130,7 @@
                         );
                     }
                     None => {
-                        error!("Registered client's bearer has been destructed ({conn_id:?})")
+                        error!("Registered client has been destructed ({conn_id:?})")
                     }
                 });
             }
@@ -169,13 +165,11 @@
 }
 #[cfg(test)]
 mod test {
-    use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
-
     use super::*;
 
     use crate::core::shared_box::SharedBox;
     use crate::gatt::mocks::mock_datastore::MockDatastore;
-    use crate::gatt::server::att_database::AttDatabase;
+    use crate::gatt::server::att_client::{AttClient, WeakAttClient};
     use crate::gatt::server::gatt_database::{
         GattDatabase, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID,
     };
@@ -193,29 +187,14 @@
         SharedBox::new(gatt_database)
     }
 
-    fn add_connection(
-        gatt_database: &SharedBox<GattDatabase>,
-        tcb_idx: TransportIndex,
-    ) -> (AttDatabaseImpl, SharedBox<AttServerBearer<AttDatabaseImpl>>, UnboundedReceiver<att::Att>)
-    {
-        let att_database = gatt_database.get_att_database(tcb_idx);
-        let (tx, rx) = unbounded_channel();
-        let bearer = SharedBox::new(AttServerBearer::new(att_database.clone(), move |packet| {
-            tx.send(packet).unwrap();
-            Ok(())
-        }));
-        gatt_database.on_bearer_ready(tcb_idx, &bearer);
-        (att_database, bearer, rx)
-    }
-
     #[test]
     fn test_gatt_service_discovery() {
         // arrange
         let gatt_db = init_gatt_db();
-        let (att_db, _, _) = add_connection(&gatt_db, TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // act: discover all services
-        let attrs = att_db.list_attributes();
+        let attrs = client.list_attributes();
 
         // assert: 1 service + 1 char decl + 1 char value + 1 char descriptor = 4 attrs
         assert_eq!(attrs.len(), 4);
@@ -239,11 +218,11 @@
     fn test_default_indication_subscription() {
         // arrange
         let gatt_db = init_gatt_db();
-        let (att_db, _, _) = add_connection(&gatt_db, TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // act: try to read the CCC descriptor
         let resp =
-            block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
+            block_on_locally(client.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
 
         assert_eq!(
             Ok(resp),
@@ -253,10 +232,10 @@
     }
 
     async fn register_for_indication(
-        att_db: &impl AttDatabase,
+        client: WeakAttClient,
         handle: AttHandle,
     ) -> Result<(), AttErrorCode> {
-        att_db
+        client
             .write_attribute(
                 handle,
                 &att::GattClientCharacteristicConfiguration { notification: 0, indication: 1 }
@@ -270,14 +249,17 @@
     fn test_subscribe_to_indication() {
         // arrange
         let gatt_db = init_gatt_db();
-        let (att_db, _, _) = add_connection(&gatt_db, TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // act: register for service change indication
-        block_on_locally(register_for_indication(&att_db, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE))
-            .unwrap();
+        block_on_locally(register_for_indication(
+            client.downgrade(),
+            SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE,
+        ))
+        .unwrap();
         // read our registration status
         let resp =
-            block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
+            block_on_locally(client.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
 
         // assert: we are registered for indications
         assert_eq!(
@@ -291,11 +273,11 @@
     fn test_unsubscribe_to_indication() {
         // arrange
         let gatt_db = init_gatt_db();
-        let (att_db, _, _) = add_connection(&gatt_db, TCB_IDX);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &gatt_db);
 
         // act: register for service change indication
         block_on_locally(
-            att_db.write_attribute(
+            client.write_attribute(
                 SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE,
                 &att::GattClientCharacteristicConfiguration { notification: 0, indication: 1 }
                     .encode_to_vec()
@@ -305,7 +287,7 @@
         .unwrap();
         // act: next, unregister from this indication
         block_on_locally(
-            att_db.write_attribute(
+            client.write_attribute(
                 SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE,
                 &att::GattClientCharacteristicConfiguration { notification: 0, indication: 0 }
                     .encode_to_vec()
@@ -315,7 +297,7 @@
         .unwrap();
         // read our registration status
         let resp =
-            block_on_locally(att_db.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
+            block_on_locally(client.read_attribute(SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)).unwrap();
 
         // assert: we are not registered for indications
         assert_eq!(
@@ -330,10 +312,12 @@
         block_on_locally(async {
             // arrange
             let gatt_db = init_gatt_db();
-            let (att_db, _bearer, mut rx) = add_connection(&gatt_db, TCB_IDX);
+            let (client, mut rx) = AttClient::new_test_client(TCB_IDX, &gatt_db);
             let (gatt_datastore, _) = MockDatastore::new();
             let gatt_datastore = Rc::new(gatt_datastore);
-            register_for_indication(&att_db, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
+            register_for_indication(client.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
 
             // act: register some new service
             gatt_db
@@ -370,11 +354,15 @@
         block_on_locally(async {
             // arrange: two connections, both registered
             let gatt_db = init_gatt_db();
-            let (att_db_1, _bearer, mut rx1) = add_connection(&gatt_db, TCB_IDX);
-            let (att_db_2, _bearer, mut rx2) = add_connection(&gatt_db, ANOTHER_TCB_IDX);
+            let (client1, mut rx1) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+            let (client2, mut rx2) = AttClient::new_test_client(ANOTHER_TCB_IDX, &gatt_db);
 
-            register_for_indication(&att_db_1, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
-            register_for_indication(&att_db_2, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
+            register_for_indication(client1.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
+            register_for_indication(client2.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
 
             let (gatt_datastore, _) = MockDatastore::new();
             let gatt_datastore = Rc::new(gatt_datastore);
@@ -409,10 +397,12 @@
         block_on_locally(async {
             // arrange: two connections, only the first is registered
             let gatt_db = init_gatt_db();
-            let (att_db_1, _bearer, mut rx1) = add_connection(&gatt_db, TCB_IDX);
-            let (_, _bearer, mut rx2) = add_connection(&gatt_db, ANOTHER_TCB_IDX);
+            let (client1, mut rx1) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+            let (_client2, mut rx2) = AttClient::new_test_client(ANOTHER_TCB_IDX, &gatt_db);
 
-            register_for_indication(&att_db_1, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
+            register_for_indication(client1.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
 
             let (gatt_datastore, _) = MockDatastore::new();
             let gatt_datastore = Rc::new(gatt_datastore);
@@ -447,14 +437,17 @@
         block_on_locally(async {
             // arrange: two connections, both register, but the second one disconnects
             let gatt_db = init_gatt_db();
-            let (att_db_1, _bearer, mut rx1) = add_connection(&gatt_db, TCB_IDX);
-            let (att_db_2, bearer_2, mut rx2) = add_connection(&gatt_db, ANOTHER_TCB_IDX);
+            let (client1, mut rx1) = AttClient::new_test_client(TCB_IDX, &gatt_db);
+            let (client2, mut rx2) = AttClient::new_test_client(ANOTHER_TCB_IDX, &gatt_db);
 
-            register_for_indication(&att_db_1, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
-            register_for_indication(&att_db_2, SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE).await.unwrap();
+            register_for_indication(client1.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
+            register_for_indication(client2.downgrade(), SERVICE_CHANGE_CCC_DESCRIPTOR_HANDLE)
+                .await
+                .unwrap();
 
-            drop(bearer_2);
-            gatt_db.on_bearer_dropped(ANOTHER_TCB_IDX);
+            drop(client2);
 
             let (gatt_datastore, _) = MockDatastore::new();
             let gatt_datastore = Rc::new(gatt_datastore);
diff --git a/system/rust/src/gatt/server/test/test_att_db.rs b/system/rust/src/gatt/server/test/test_att_db.rs
index 2011baf..6f74568 100644
--- a/system/rust/src/gatt/server/test/test_att_db.rs
+++ b/system/rust/src/gatt/server/test/test_att_db.rs
@@ -1,84 +1,132 @@
-use crate::gatt::ids::AttHandle;
-use crate::gatt::server::att_database::{AttAttribute, AttDatabase, StableAttDatabase};
+use crate::core::shared_box::SharedBox;
+use crate::gatt::callbacks::{GattWriteRequestType, TransactionDecision};
+use crate::gatt::ffi::AttributeBackingType;
+use crate::gatt::ids::{AttHandle, TransportIndex};
+use crate::gatt::server::att_database::AttAttribute;
+use crate::gatt::server::gatt_database::GattDatabase;
+use crate::gatt::server::RawGattDatastore;
 use crate::packets::att::AttErrorCode;
 
 use async_trait::async_trait;
-use log::{info, warn};
 use std::cell::RefCell;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 use std::rc::Rc;
 
-#[derive(Clone, Debug)]
-pub struct TestAttDatabase {
-    attributes: Rc<BTreeMap<AttHandle, TestAttributeWithData>>,
+pub struct TestDatastore {
+    values: BTreeMap<AttHandle, RefCell<Vec<u8>>>,
+    queued_writes: RefCell<HashMap<TransportIndex, Vec<(AttHandle, u32, Vec<u8>)>>>,
 }
 
-#[derive(Debug)]
-struct TestAttributeWithData {
-    attribute: AttAttribute,
-    data: RefCell<Vec<u8>>,
-}
-
-impl TestAttDatabase {
-    pub fn new(attributes: Vec<(AttAttribute, Vec<u8>)>) -> Self {
-        Self {
-            attributes: Rc::new(
-                attributes
-                    .into_iter()
-                    .map(|(attribute, data)| {
-                        (attribute.handle, TestAttributeWithData { attribute, data: data.into() })
-                    })
-                    .collect(),
-            ),
-        }
+impl TestDatastore {
+    pub fn new(values: impl IntoIterator<Item = (AttHandle, Vec<u8>)>) -> Rc<Self> {
+        Rc::new(Self {
+            values: values.into_iter().map(|(handle, data)| (handle, RefCell::new(data))).collect(),
+            queued_writes: RefCell::default(),
+        })
     }
-}
 
-#[async_trait(?Send)]
-impl AttDatabase for TestAttDatabase {
-    async fn read_attribute(&self, handle: AttHandle) -> Result<Vec<u8>, AttErrorCode> {
-        info!("reading {handle:?}");
-        match self.attributes.get(&handle) {
-            Some(TestAttributeWithData { attribute: AttAttribute { permissions, .. }, .. })
-                if !permissions.readable() =>
-            {
-                Err(AttErrorCode::ReadNotPermitted)
-            }
-            Some(TestAttributeWithData { data, .. }) => Ok(data.borrow().clone()),
-            None => Err(AttErrorCode::InvalidHandle),
-        }
-    }
-    async fn write_attribute(&self, handle: AttHandle, data: &[u8]) -> Result<(), AttErrorCode> {
-        match self.attributes.get(&handle) {
-            Some(TestAttributeWithData { attribute: AttAttribute { permissions, .. }, .. })
-                if !permissions.writable_with_response() =>
-            {
-                Err(AttErrorCode::WriteNotPermitted)
-            }
-            Some(TestAttributeWithData { data: data_cell, .. }) => {
-                data_cell.replace(data.to_vec());
+    fn write_impl(&self, handle: AttHandle, data: &[u8]) -> Result<(), AttErrorCode> {
+        match self.values.get(&handle) {
+            Some(value) => {
+                *value.borrow_mut() = data.into();
                 Ok(())
             }
             None => Err(AttErrorCode::InvalidHandle),
         }
     }
-    fn write_no_response_attribute(&self, handle: AttHandle, data: &[u8]) {
-        match self.attributes.get(&handle) {
-            Some(TestAttributeWithData {
-                attribute: AttAttribute { permissions, .. },
-                data: data_cell,
-            }) if !permissions.writable_with_response() => {
-                data_cell.replace(data.to_vec());
-            }
-            _ => {
-                warn!("rejecting write command to {handle:?}")
+}
+
+#[async_trait(?Send)]
+impl RawGattDatastore for TestDatastore {
+    async fn read(
+        &self,
+        _tcb_idx: TransportIndex,
+        handle: AttHandle,
+        offset: u32,
+        attr_type: AttributeBackingType,
+    ) -> Result<Vec<u8>, AttErrorCode> {
+        assert_eq!(offset, 0);
+        assert_eq!(attr_type, AttributeBackingType::Characteristic);
+        match self.values.get(&handle) {
+            Some(value) => Ok(value.borrow().clone()),
+            None => Err(AttErrorCode::InvalidHandle),
+        }
+    }
+
+    async fn write(
+        &self,
+        tcb_idx: TransportIndex,
+        handle: AttHandle,
+        attr_type: AttributeBackingType,
+        write_type: GattWriteRequestType,
+        data: &[u8],
+    ) -> Result<(), AttErrorCode> {
+        assert_eq!(attr_type, AttributeBackingType::Characteristic);
+        match write_type {
+            GattWriteRequestType::Request => self.write_impl(handle, data),
+            GattWriteRequestType::Prepare { offset } => {
+                if !self.values.contains_key(&handle) {
+                    return Err(AttErrorCode::InvalidHandle);
+                }
+                self.queued_writes.borrow_mut().entry(tcb_idx).or_default().push((
+                    handle,
+                    offset,
+                    data.to_vec(),
+                ));
+                Ok(())
             }
         }
     }
-    fn list_attributes(&self) -> Vec<AttAttribute> {
-        self.attributes.values().map(|attr| attr.attribute).collect()
+
+    fn write_no_response(
+        &self,
+        _tcb_idx: TransportIndex,
+        handle: AttHandle,
+        attr_type: AttributeBackingType,
+        data: &[u8],
+    ) {
+        assert_eq!(attr_type, AttributeBackingType::Characteristic);
+        let _ = self.write_impl(handle, data);
+    }
+
+    async fn execute(
+        &self,
+        tcb_idx: TransportIndex,
+        decision: TransactionDecision,
+    ) -> Result<(), AttErrorCode> {
+        if let Some(writes) = self.queued_writes.borrow_mut().remove(&tcb_idx) {
+            if decision == TransactionDecision::Cancel {
+                return Ok(());
+            }
+            for (handle, offset, data) in &writes {
+                let value = self.values[handle].borrow();
+                let offset = *offset as usize;
+                if offset > value.len() {
+                    return Err(AttErrorCode::InvalidOffset);
+                }
+                if offset + data.len() > value.len() {
+                    return Err(AttErrorCode::InvalidAttributeValueLength);
+                }
+            }
+            for (handle, offset, data) in writes {
+                let offset = offset as usize;
+                self.values[&handle].borrow_mut()[offset..offset + data.len()]
+                    .copy_from_slice(&data);
+            }
+        }
+        Ok(())
     }
 }
 
-// We guarantee that the contents of a TestAttDatabase will remain stable
-impl StableAttDatabase for TestAttDatabase {}
+/// Creates a new test database with the specified characteristics.
+pub fn new_test_database(
+    mut characteristics: Vec<(AttAttribute, Vec<u8>)>,
+) -> SharedBox<GattDatabase> {
+    let datastore = TestDatastore::new(
+        characteristics.iter_mut().map(|(a, data)| (a.handle, std::mem::take(data))),
+    );
+    SharedBox::new(GattDatabase::with_characteristics(
+        characteristics.into_iter().map(|(a, _)| a),
+        datastore,
+    ))
+}
diff --git a/system/rust/src/gatt/server/transactions/find_by_type_value.rs b/system/rust/src/gatt/server/transactions/find_by_type_value.rs
index c967158..994fcf6 100644
--- a/system/rust/src/gatt/server/transactions/find_by_type_value.rs
+++ b/system/rust/src/gatt/server/transactions/find_by_type_value.rs
@@ -3,7 +3,8 @@
 
 use crate::core::uuid::Uuid;
 use crate::gatt::ids::AttHandle;
-use crate::gatt::server::att_database::{AttAttribute, StableAttDatabase};
+use crate::gatt::server::att_client::WeakAttClient;
+use crate::gatt::server::att_database::AttAttribute;
 use crate::packets::att::{self, AttErrorCode};
 
 use super::helpers::att_grouping::find_group_end;
@@ -13,12 +14,13 @@
 pub async fn handle_find_by_type_value_request(
     request: att::AttFindByTypeValueRequest,
     mtu: usize,
-    db: &impl StableAttDatabase,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
-    let Some(attrs) = filter_to_range(
+    let attrs = client.list_attributes();
+    let Some(filtered) = filter_to_range(
         request.starting_handle.clone().into(),
         request.ending_handle.into(),
-        db.list_attributes().into_iter(),
+        attrs.iter(),
     ) else {
         return att::AttErrorResponse {
             opcode_in_error: att::AttOpcode::FindByTypeValueRequest,
@@ -31,18 +33,18 @@
     // ATT_MTU-1 limit comes from Spec 5.3 Vol 3F Sec 3.4.3.4
     let mut matches = PayloadAccumulator::new(mtu - 1);
 
-    for attr @ AttAttribute { handle, type_, .. } in attrs {
-        if Uuid::from(request.attribute_type.clone()) != type_ {
+    for attr @ AttAttribute { handle, type_, .. } in filtered {
+        if &Uuid::from(request.attribute_type.clone()) != type_ {
             continue;
         }
-        if let Ok(value) = db.read_attribute(handle).await {
+        if let Ok(value) = client.read_attribute(*handle).await {
             if value == request.attribute_value {
                 // match found
                 if !matches.push(att::AttributeHandleRange {
-                    found_attribute_handle: handle.into(),
-                    group_end_handle: find_group_end(db, attr)
+                    found_attribute_handle: (*handle).into(),
+                    group_end_handle: find_group_end(&attrs, attr)
                         .map(|attr| attr.handle)
-                        .unwrap_or(handle)
+                        .unwrap_or(*handle)
                         .into(),
                 }) {
                     break;
@@ -68,10 +70,12 @@
 #[cfg(test)]
 mod test {
     use crate::gatt::ffi::Uuid;
+    use crate::gatt::ids::TransportIndex;
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::gatt_database::{
         AttPermissions, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID,
     };
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
 
     use super::*;
@@ -81,11 +85,12 @@
 
     const VALUE: [u8; 2] = [1, 2];
     const ANOTHER_VALUE: [u8; 2] = [3, 4];
+    const TCB_IDX: TransportIndex = TransportIndex(1);
 
     #[test]
     fn test_uuid_match() {
         // arrange: db all with same value, but some with different UUID
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -111,6 +116,7 @@
                 VALUE.into(),
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttFindByTypeValueRequest {
@@ -119,7 +125,11 @@
             attribute_type: UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 128, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            128,
+            &client.downgrade(),
+        ));
 
         // assert: we only matched the ones with the correct UUID
         assert_eq!(
@@ -143,7 +153,7 @@
     #[test]
     fn test_value_match() {
         // arrange: db all with same type, but some with different value
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -169,6 +179,7 @@
                 VALUE.into(),
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttFindByTypeValueRequest {
@@ -177,7 +188,11 @@
             attribute_type: UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 128, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            128,
+            &client.downgrade(),
+        ));
 
         // assert
         assert_eq!(
@@ -201,7 +216,8 @@
     #[test]
     fn test_range_check() {
         // arrange: empty db
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: provide an invalid handle range
         let att_view = att::AttFindByTypeValueRequest {
@@ -210,7 +226,11 @@
             attribute_type: UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 128, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            128,
+            &client.downgrade(),
+        ));
 
         // assert
         assert_eq!(
@@ -227,7 +247,7 @@
     #[test]
     fn test_empty_response() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: UUID,
@@ -235,6 +255,7 @@
             },
             VALUE.into(),
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: query using a range that does not overlap with matching attributes
         let att_view = att::AttFindByTypeValueRequest {
@@ -243,7 +264,11 @@
             attribute_type: UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 128, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            128,
+            &client.downgrade(),
+        ));
 
         // assert: got ATTRIBUTE_NOT_FOUND error
         assert_eq!(
@@ -260,7 +285,7 @@
     #[test]
     fn test_grouping_uuid() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -286,6 +311,7 @@
                 VALUE.into(),
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: look for a particular characteristic declaration
         let att_view = att::AttFindByTypeValueRequest {
@@ -294,7 +320,11 @@
             attribute_type: CHARACTERISTIC_UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 128, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            128,
+            &client.downgrade(),
+        ));
 
         // assert
         assert_eq!(
@@ -312,7 +342,7 @@
     #[test]
     fn test_limit_total_size() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -330,6 +360,7 @@
                 VALUE.into(),
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: use MTU = 5, so we can only fit one element in the output
         let att_view = att::AttFindByTypeValueRequest {
@@ -338,7 +369,11 @@
             attribute_type: UUID.try_into().unwrap(),
             attribute_value: VALUE.to_vec(),
         };
-        let response = tokio_test::block_on(handle_find_by_type_value_request(att_view, 5, &db));
+        let response = tokio_test::block_on(handle_find_by_type_value_request(
+            att_view,
+            5,
+            &client.downgrade(),
+        ));
 
         // assert: only one of the two matches produced
         assert_eq!(
diff --git a/system/rust/src/gatt/server/transactions/find_information_request.rs b/system/rust/src/gatt/server/transactions/find_information_request.rs
index b33cacc..34ecb0d 100644
--- a/system/rust/src/gatt/server/transactions/find_information_request.rs
+++ b/system/rust/src/gatt/server/transactions/find_information_request.rs
@@ -1,19 +1,21 @@
-use crate::gatt::server::att_database::{AttAttribute, AttDatabase};
+use crate::gatt::server::att_client::WeakAttClient;
+use crate::gatt::server::att_database::AttAttribute;
 use crate::packets::att::{self, AttErrorCode};
 use pdl_runtime::EncodeError;
 
 use super::helpers::att_range_filter::filter_to_range;
 use super::helpers::payload_accumulator::PayloadAccumulator;
 
-pub fn handle_find_information_request<T: AttDatabase>(
+pub fn handle_find_information_request(
     request: att::AttFindInformationRequest,
     mtu: usize,
-    db: &T,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
+    let attrs = client.list_attributes();
     let Some(attrs) = filter_to_range(
         request.starting_handle.clone().into(),
         request.ending_handle.into(),
-        db.list_attributes().into_iter(),
+        attrs.iter(),
     ) else {
         return att::AttErrorResponse {
             opcode_in_error: att::AttOpcode::FindInformationRequest,
@@ -39,15 +41,17 @@
 
 /// Returns a builder IF we can return at least one attribute, otherwise returns
 /// None
-fn handle_find_information_request_short(
-    attributes: impl Iterator<Item = AttAttribute>,
+fn handle_find_information_request_short<'a>(
+    attributes: impl Iterator<Item = &'a AttAttribute>,
     mtu: usize,
 ) -> Option<att::AttFindInformationShortResponse> {
     // Core Spec 5.3 Vol 3F 3.4.3.2 gives the ATT_MTU - 2 limit
     let mut out = PayloadAccumulator::new(mtu - 2);
     for AttAttribute { handle, type_: uuid, .. } in attributes {
-        if let Ok(uuid) = uuid.try_into() {
-            if out.push(att::AttFindInformationResponseShortEntry { handle: handle.into(), uuid }) {
+        if let Ok(uuid) = (*uuid).try_into() {
+            if out
+                .push(att::AttFindInformationResponseShortEntry { handle: (*handle).into(), uuid })
+            {
                 // If we successfully pushed a 16-bit UUID, continue. In all other cases, we
                 // should break.
                 continue;
@@ -63,8 +67,8 @@
     }
 }
 
-fn handle_find_information_request_long(
-    attributes: impl Iterator<Item = AttAttribute>,
+fn handle_find_information_request_long<'a>(
+    attributes: impl Iterator<Item = &'a AttAttribute>,
     mtu: usize,
 ) -> Option<att::AttFindInformationLongResponse> {
     // Core Spec 5.3 Vol 3F 3.4.3.2 gives the ATT_MTU - 2 limit
@@ -72,8 +76,8 @@
 
     for AttAttribute { handle, type_: uuid, .. } in attributes {
         if !out.push(att::AttFindInformationResponseLongEntry {
-            handle: handle.into(),
-            uuid: uuid.into(),
+            handle: (*handle).into(),
+            uuid: (*uuid).into(),
         }) {
             break;
         }
@@ -89,17 +93,21 @@
 #[cfg(test)]
 mod test {
     use crate::core::uuid::Uuid;
+    use crate::gatt::ids::TransportIndex;
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::gatt::server::AttHandle;
     use crate::packets::att;
 
     use super::*;
 
+    const TCB_IDX: TransportIndex = TransportIndex(1);
+
     #[test]
     fn test_long_uuids() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -125,13 +133,14 @@
                 vec![4, 5],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttFindInformationRequest {
             starting_handle: AttHandle(3).into(),
             ending_handle: AttHandle(4).into(),
         };
-        let response = handle_find_information_request(att_view, 128, &db);
+        let response = handle_find_information_request(att_view, 128, &client.downgrade());
 
         // assert
         assert_eq!(
@@ -155,7 +164,7 @@
     #[test]
     fn test_short_uuids() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -181,13 +190,14 @@
                 vec![4, 5],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttFindInformationRequest {
             starting_handle: AttHandle(3).into(),
             ending_handle: AttHandle(5).into(),
         };
-        let response = handle_find_information_request(att_view, 128, &db);
+        let response = handle_find_information_request(att_view, 128, &client.downgrade());
 
         // assert
         assert_eq!(
@@ -211,14 +221,15 @@
     #[test]
     fn test_handle_validation() {
         // arrange: empty db
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: use an invalid handle range
         let att_view = att::AttFindInformationRequest {
             starting_handle: AttHandle(3).into(),
             ending_handle: AttHandle(2).into(),
         };
-        let response = handle_find_information_request(att_view, 128, &db);
+        let response = handle_find_information_request(att_view, 128, &client.downgrade());
 
         // assert: got INVALID_HANDLE
         assert_eq!(
@@ -235,7 +246,7 @@
     #[test]
     fn test_limit_total_size() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -253,13 +264,14 @@
                 vec![4, 5],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: use MTU = 6, so only one entry can fit
         let att_view = att::AttFindInformationRequest {
             starting_handle: AttHandle(3).into(),
             ending_handle: AttHandle(5).into(),
         };
-        let response = handle_find_information_request(att_view, 6, &db);
+        let response = handle_find_information_request(att_view, 6, &client.downgrade());
 
         // assert: only one entry (not two) provided
         assert_eq!(
@@ -277,7 +289,7 @@
     #[test]
     fn test_empty_output() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: Uuid::new(0x0102),
@@ -285,13 +297,14 @@
             },
             vec![4, 5],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: use a range that matches no attributes
         let att_view = att::AttFindInformationRequest {
             starting_handle: AttHandle(4).into(),
             ending_handle: AttHandle(5).into(),
         };
-        let response = handle_find_information_request(att_view, 6, &db);
+        let response = handle_find_information_request(att_view, 6, &client.downgrade());
 
         // assert: got ATTRIBUTE_NOT_FOUND
         assert_eq!(
diff --git a/system/rust/src/gatt/server/transactions/helpers/att_filter_by_size_type.rs b/system/rust/src/gatt/server/transactions/helpers/att_filter_by_size_type.rs
index 1e8870c..12153a9 100644
--- a/system/rust/src/gatt/server/transactions/helpers/att_filter_by_size_type.rs
+++ b/system/rust/src/gatt/server/transactions/helpers/att_filter_by_size_type.rs
@@ -2,7 +2,8 @@
 //! length, used in READ_BY_TYPE_REQ and READ_BY_GROUP_TYPE_REQ
 
 use crate::core::uuid::Uuid;
-use crate::gatt::server::att_database::{AttAttribute, StableAttDatabase};
+use crate::gatt::server::att_client::WeakAttClient;
+use crate::gatt::server::att_database::AttAttribute;
 use crate::packets::att::AttErrorCode;
 
 /// An attribute and the value
@@ -23,18 +24,18 @@
 /// Attributes are truncated to the attr_size limit before size comparison.
 /// If an error occurs while reading, do not output further attributes.
 pub async fn filter_read_attributes_by_size_type(
-    db: &impl StableAttDatabase,
-    attrs: impl Iterator<Item = AttAttribute>,
+    client: &WeakAttClient,
+    attrs: impl IntoIterator<Item = &AttAttribute>,
     target: Uuid,
     size_limit: usize,
 ) -> Result<impl Iterator<Item = AttributeWithValue>, AttErrorCode> {
-    let target_attrs = attrs.filter(|attr| attr.type_ == target);
+    let target_attrs = attrs.into_iter().filter(|attr| attr.type_ == target);
 
     let mut out = vec![];
     let mut curr_elem_size = None;
 
     for attr @ AttAttribute { handle, .. } in target_attrs {
-        match db.read_attribute(handle).await {
+        match client.read_attribute(*handle).await {
             Ok(mut value) => {
                 value.truncate(size_limit);
                 let value_size = value.len();
@@ -47,7 +48,7 @@
                     curr_elem_size = Some(value_size)
                 }
 
-                out.push(AttributeWithValue { attr, value });
+                out.push(AttributeWithValue { attr: *attr, value });
             }
             Err(err) => {
                 if out.is_empty() {
@@ -66,18 +67,20 @@
     use super::*;
 
     use crate::core::uuid::Uuid;
-    use crate::gatt::ids::AttHandle;
-    use crate::gatt::server::att_database::{AttAttribute, AttDatabase, StableAttDatabase};
+    use crate::gatt::ids::{AttHandle, TransportIndex};
+    use crate::gatt::server::att_client::AttClient;
+    use crate::gatt::server::att_database::AttAttribute;
     use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
 
     const UUID: Uuid = Uuid::new(1234);
     const ANOTHER_UUID: Uuid = Uuid::new(2345);
+    const TCB_IDX: TransportIndex = TransportIndex(1);
 
     #[test]
     fn test_single_matching_attr() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: UUID,
@@ -85,21 +88,26 @@
             },
             vec![4, 5],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![AttributeWithValue {
-                attr: db.find_attribute(AttHandle(3)).unwrap(),
+                attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                 value: vec![4, 5],
             }]
         )
@@ -108,7 +116,7 @@
     #[test]
     fn test_skip_mismatching_attrs() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -134,26 +142,31 @@
                 vec![6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![
                 AttributeWithValue {
-                    attr: db.find_attribute(AttHandle(3)).unwrap(),
+                    attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                     value: vec![4, 5],
                 },
                 AttributeWithValue {
-                    attr: db.find_attribute(AttHandle(6)).unwrap(),
+                    attr: db.get(AttHandle(6)).map(|a| a.attribute).unwrap(),
                     value: vec![6, 7],
                 }
             ]
@@ -163,7 +176,7 @@
     #[test]
     fn test_stop_once_length_changes() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -182,28 +195,33 @@
             ),
             (
                 AttAttribute {
-                    handle: AttHandle(6),
+                    handle: AttHandle(7),
                     type_: UUID,
                     permissions: AttPermissions::READABLE,
                 },
                 vec![6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![AttributeWithValue {
-                attr: db.find_attribute(AttHandle(3)).unwrap(),
+                attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                 value: vec![4, 5],
             },]
         );
@@ -212,7 +230,7 @@
     #[test]
     fn test_truncate_to_mtu() {
         // arrange: attr with data of length 3
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: UUID,
@@ -220,21 +238,26 @@
             },
             vec![4, 5, 6],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: read the attribute with max_size = 2
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            2,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                2,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert: the length of the read attribute is 2
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![AttributeWithValue {
-                attr: db.find_attribute(AttHandle(3)).unwrap(),
+                attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                 value: vec![4, 5],
             },]
         );
@@ -243,25 +266,30 @@
     #[test]
     fn test_no_results() {
         // arrange: an empty database
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert: no results
-        assert_eq!(response.count(), 0)
+        assert!(response.is_empty())
     }
 
     #[test]
     fn test_read_failure_on_first_attr() {
         // arrange: put a non-readable attribute in the db with the right type
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: UUID,
@@ -269,11 +297,15 @@
             },
             vec![4, 5, 6],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
+
+        let weak_client = client.downgrade();
+        let attrs = client.list_attributes();
 
         // act
         let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
+            &weak_client,
+            &attrs,
             UUID,
             31,
         ));
@@ -286,7 +318,7 @@
     fn test_read_failure_on_subsequent_attr() {
         // arrange: put a non-readable attribute in the db with the right
         // type
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -312,22 +344,27 @@
                 vec![8, 9, 10],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert: we reply with the first attribute, but not the second or third
         // (since we stop on the first failure)
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![AttributeWithValue {
-                attr: db.find_attribute(AttHandle(3)).unwrap(),
+                attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                 value: vec![4, 5, 6],
             },]
         );
@@ -337,7 +374,7 @@
     fn test_skip_unreadable_mismatching_attr() {
         // arrange: put a non-readable attribute in the db with the wrong type
         // between two attributes of interest
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -363,26 +400,31 @@
                 vec![6, 7, 8],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = tokio_test::block_on(filter_read_attributes_by_size_type(
-            &db,
-            db.list_attributes().into_iter(),
-            UUID,
-            31,
-        ))
-        .unwrap();
+        let response: Vec<_> = tokio_test::block_on(async {
+            filter_read_attributes_by_size_type(
+                &client.downgrade(),
+                &client.list_attributes(),
+                UUID,
+                31,
+            )
+            .await
+            .unwrap()
+            .collect()
+        });
 
         // assert: we reply with the first and third attributes, but not the second
         assert_eq!(
-            response.collect::<Vec<_>>(),
+            response,
             vec![
                 AttributeWithValue {
-                    attr: db.find_attribute(AttHandle(3)).unwrap(),
+                    attr: db.get(AttHandle(3)).map(|a| a.attribute).unwrap(),
                     value: vec![4, 5, 6],
                 },
                 AttributeWithValue {
-                    attr: db.find_attribute(AttHandle(5)).unwrap(),
+                    attr: db.get(AttHandle(5)).map(|a| a.attribute).unwrap(),
                     value: vec![6, 7, 8],
                 }
             ]
diff --git a/system/rust/src/gatt/server/transactions/helpers/att_grouping.rs b/system/rust/src/gatt/server/transactions/helpers/att_grouping.rs
index 1596475..2374c4f 100644
--- a/system/rust/src/gatt/server/transactions/helpers/att_grouping.rs
+++ b/system/rust/src/gatt/server/transactions/helpers/att_grouping.rs
@@ -4,7 +4,7 @@
 
 use crate::core::uuid::Uuid;
 
-use crate::gatt::server::att_database::{AttAttribute, StableAttDatabase};
+use crate::gatt::server::att_database::AttAttribute;
 use crate::gatt::server::gatt_database::{
     CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID, SECONDARY_SERVICE_DECLARATION_UUID,
 };
@@ -29,16 +29,16 @@
 /// Expects `attrs` to be in sorted order by attribute handle.
 ///
 /// Attribute grouping is defined in 5.3 Vol 3G Sec 2.5.3 Attribute Grouping
-pub fn find_group_end(
-    db: &impl StableAttDatabase,
-    group_start: AttAttribute,
-) -> Option<AttAttribute> {
+pub fn find_group_end<'a>(
+    attributes: impl IntoIterator<Item = &'a AttAttribute>,
+    group_start: &'a AttAttribute,
+) -> Option<&'a AttAttribute> {
     if !GROUPING_ATTRIBUTES.contains(&group_start.type_) {
         return None; // invalid / unsupported grouping attribute
     }
 
     Some(
-        db.list_attributes()
+        attributes
             .into_iter()
             // ignore attributes at or before the current position
             .skip_while(|attr| attr.handle <= group_start.handle)
@@ -54,16 +54,17 @@
 
 #[cfg(test)]
 mod test {
+    use crate::core::shared_box::SharedBox;
     use crate::gatt::ids::AttHandle;
-    use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::gatt_database::{AttPermissions, GattDatabase};
+    use crate::gatt::server::test::test_att_db::new_test_database;
 
     use super::*;
 
     const OTHER_UUID: Uuid = Uuid::new(1234);
 
-    fn db_from_attrs(attrs: impl IntoIterator<Item = AttAttribute>) -> TestAttDatabase {
-        TestAttDatabase::new(attrs.into_iter().map(|attr| (attr, vec![])).collect())
+    fn db_from_attrs(attrs: impl IntoIterator<Item = AttAttribute>) -> SharedBox<GattDatabase> {
+        new_test_database(attrs.into_iter().map(|attr| (attr, vec![])).collect())
     }
 
     fn attr(handle: AttHandle, type_: Uuid) -> AttAttribute {
@@ -80,7 +81,9 @@
             attr(AttHandle(40), PRIMARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(30))
     }
@@ -94,7 +97,9 @@
             attr(AttHandle(40), SECONDARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(30))
     }
@@ -109,8 +114,9 @@
             attr(AttHandle(50), PRIMARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end =
-            find_group_end(&db, attr(AttHandle(10), SECONDARY_SERVICE_DECLARATION_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), SECONDARY_SERVICE_DECLARATION_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(40))
     }
@@ -125,8 +131,9 @@
             attr(AttHandle(50), SECONDARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end =
-            find_group_end(&db, attr(AttHandle(10), SECONDARY_SERVICE_DECLARATION_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), SECONDARY_SERVICE_DECLARATION_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(40))
     }
@@ -139,7 +146,9 @@
             attr(AttHandle(30), SECONDARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), CHARACTERISTIC_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), CHARACTERISTIC_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(20))
     }
@@ -152,7 +161,9 @@
             attr(AttHandle(30), CHARACTERISTIC_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), CHARACTERISTIC_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), CHARACTERISTIC_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(20))
     }
@@ -165,7 +176,9 @@
             attr(AttHandle(30), OTHER_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), PRIMARY_SERVICE_DECLARATION_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(30))
     }
@@ -174,7 +187,9 @@
     fn test_empty_non_terminated_group() {
         let db = db_from_attrs([attr(AttHandle(10), CHARACTERISTIC_UUID)]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), CHARACTERISTIC_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), CHARACTERISTIC_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(10))
     }
@@ -186,7 +201,9 @@
             attr(AttHandle(20), SECONDARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), CHARACTERISTIC_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), CHARACTERISTIC_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end.unwrap().handle, AttHandle(10))
     }
@@ -198,7 +215,9 @@
             attr(AttHandle(20), SECONDARY_SERVICE_DECLARATION_UUID),
         ]);
 
-        let group_end = find_group_end(&db, attr(AttHandle(10), OTHER_UUID));
+        let attrs = db.list_attributes();
+        let group_start = attr(AttHandle(10), OTHER_UUID);
+        let group_end = find_group_end(&attrs, &group_start);
 
         assert_eq!(group_end, None)
     }
diff --git a/system/rust/src/gatt/server/transactions/helpers/att_range_filter.rs b/system/rust/src/gatt/server/transactions/helpers/att_range_filter.rs
index b70119b..9da1e35 100644
--- a/system/rust/src/gatt/server/transactions/helpers/att_range_filter.rs
+++ b/system/rust/src/gatt/server/transactions/helpers/att_range_filter.rs
@@ -8,11 +8,11 @@
 /// Filter a (sorted) iterator of attributes to those that lie within
 /// the specified range. If the range is invalid (start = 0, or start > end),
 /// return None.
-pub fn filter_to_range(
+pub fn filter_to_range<'a>(
     start_handle: AttHandle,
     end_handle: AttHandle,
-    attrs: impl Iterator<Item = AttAttribute> + Clone,
-) -> Option<impl Iterator<Item = AttAttribute> + Clone> {
+    attrs: impl Iterator<Item = &'a AttAttribute> + Clone,
+) -> Option<impl Iterator<Item = &'a AttAttribute> + Clone> {
     if start_handle.0 == 0 || end_handle < start_handle {
         return None;
     }
@@ -56,22 +56,17 @@
     fn test_trivial_range() {
         // call with a range where start == end, make sure it gets the relevant
         // attribute
-        let res =
-            filter_to_range(AttHandle(3), AttHandle(3), [attr(2), attr(3), attr(4)].into_iter())
-                .unwrap();
+        let attrs = [attr(2), attr(3), attr(4)];
+        let res = filter_to_range(AttHandle(3), AttHandle(3), attrs.iter()).unwrap();
 
-        assert_eq!(res.collect::<Vec<_>>(), vec![attr(3)])
+        assert_eq!(res.cloned().collect::<Vec<_>>(), vec![attr(3)])
     }
 
     #[test]
     fn test_nontrivial_range() {
-        let res = filter_to_range(
-            AttHandle(3),
-            AttHandle(4),
-            [attr(2), attr(3), attr(4), attr(5)].into_iter(),
-        )
-        .unwrap();
+        let attrs = [attr(2), attr(3), attr(4), attr(5)];
+        let res = filter_to_range(AttHandle(3), AttHandle(4), attrs.iter()).unwrap();
 
-        assert_eq!(res.collect::<Vec<_>>(), vec![attr(3), attr(4)])
+        assert_eq!(res.cloned().collect::<Vec<_>>(), vec![attr(3), attr(4)])
     }
 }
diff --git a/system/rust/src/gatt/server/transactions/read_by_group_type_request.rs b/system/rust/src/gatt/server/transactions/read_by_group_type_request.rs
index 7bff62c..6e6ee07 100644
--- a/system/rust/src/gatt/server/transactions/read_by_group_type_request.rs
+++ b/system/rust/src/gatt/server/transactions/read_by_group_type_request.rs
@@ -1,5 +1,5 @@
 use crate::core::uuid::Uuid;
-use crate::gatt::server::att_database::StableAttDatabase;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::gatt::server::gatt_database::{
     PRIMARY_SERVICE_DECLARATION_UUID, SECONDARY_SERVICE_DECLARATION_UUID,
 };
@@ -16,7 +16,7 @@
 pub async fn handle_read_by_group_type_request(
     request: att::AttReadByGroupTypeRequest,
     mtu: usize,
-    db: &impl StableAttDatabase,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     // As per spec (5.3 Vol 3F 3.4.4.9)
     // > If an attribute in the set of requested attributes would cause an
@@ -38,10 +38,11 @@
         return failure_response.try_into();
     };
 
-    let Some(attrs) = filter_to_range(
+    let all_attrs = client.list_attributes();
+    let Some(filtered) = filter_to_range(
         request.starting_handle.into(),
         request.ending_handle.into(),
-        db.list_attributes().into_iter(),
+        all_attrs.iter(),
     ) else {
         failure_response.error_code = AttErrorCode::InvalidHandle;
         return failure_response.try_into();
@@ -60,12 +61,12 @@
     let mut matches = PayloadAccumulator::new(mtu - 2);
 
     // MTU-6 limit comes from Core Spec 5.3 Vol 3F 3.4.4.9
-    match filter_read_attributes_by_size_type(db, attrs, group_type, mtu - 6).await {
+    match filter_read_attributes_by_size_type(client, filtered, group_type, mtu - 6).await {
         Ok(attrs) => {
             for AttributeWithValue { attr, value } in attrs {
                 if !matches.push(att::AttReadByGroupTypeDataElement {
                     handle: attr.handle.into(),
-                    end_group_handle: find_group_end(db, attr)
+                    end_group_handle: find_group_end(&all_attrs, &attr)
                         .expect("should never be None, since grouping UUID was validated earlier")
                         .handle
                         .into(),
@@ -90,18 +91,20 @@
 
 #[cfg(test)]
 mod test {
-    use crate::gatt::ids::AttHandle;
+    use crate::gatt::ids::{AttHandle, TransportIndex};
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::AttAttribute;
     use crate::gatt::server::gatt_database::{AttPermissions, CHARACTERISTIC_UUID};
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
 
     use super::*;
+    const TCB_IDX: TransportIndex = TransportIndex(1);
 
     #[test]
     fn test_simple_grouping() {
         // arrange: one service with a child attribute, another service with no children
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -127,6 +130,7 @@
                 vec![6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByGroupTypeRequest {
@@ -134,7 +138,11 @@
             ending_handle: AttHandle(6).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 31, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            31,
+            &client.downgrade(),
+        ));
 
         // assert: we identified both service groups
         assert_eq!(
@@ -160,7 +168,8 @@
     #[test]
     fn test_invalid_group_type() {
         // arrange
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: try using an unsupported group type
         let att_view = att::AttReadByGroupTypeRequest {
@@ -168,7 +177,11 @@
             ending_handle: AttHandle(6).into(),
             attribute_group_type: CHARACTERISTIC_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 31, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            31,
+            &client.downgrade(),
+        ));
 
         // assert: got UNSUPPORTED_GROUP_TYPE
         assert_eq!(
@@ -185,7 +198,8 @@
     #[test]
     fn test_range_validation() {
         // arrange: an empty (irrelevant) db
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: query with an invalid attribute range
         let att_view = att::AttReadByGroupTypeRequest {
@@ -193,7 +207,11 @@
             ending_handle: AttHandle(2).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 31, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            31,
+            &client.downgrade(),
+        ));
 
         // assert: we return an INVALID_HANDLE error
         assert_eq!(
@@ -210,7 +228,7 @@
     #[test]
     fn test_attribute_truncation() {
         // arrange: one service with a value of length 5
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: PRIMARY_SERVICE_DECLARATION_UUID,
@@ -218,6 +236,7 @@
             },
             vec![1, 2, 3, 4, 5],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: read the service value with MTU = 7, so the value is truncated to MTU-6
         // = 1
@@ -226,7 +245,11 @@
             ending_handle: AttHandle(6).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 7, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            7,
+            &client.downgrade(),
+        ));
 
         // assert: we identified both service groups
         assert_eq!(
@@ -245,7 +268,7 @@
     #[test]
     fn test_limit_total_size() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -263,6 +286,7 @@
                 vec![5, 6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: read with MTU = 9, so we can only fit the first attribute (untruncated)
         let att_view = att::AttReadByGroupTypeRequest {
@@ -270,7 +294,11 @@
             ending_handle: AttHandle(6).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 9, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            9,
+            &client.downgrade(),
+        ));
 
         // assert: we return only the first attribute
         assert_eq!(
@@ -289,7 +317,7 @@
     #[test]
     fn test_group_end_outside_range() {
         // arrange: one service with a child attribute
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -307,6 +335,7 @@
                 vec![5, 6],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: search in an interval that includes the service but not its child
         let att_view = att::AttReadByGroupTypeRequest {
@@ -314,7 +343,11 @@
             ending_handle: AttHandle(3).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 31, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            31,
+            &client.downgrade(),
+        ));
 
         // assert: the end_group_handle is correct, even though it exceeds the query
         // interval
@@ -334,7 +367,7 @@
     #[test]
     fn test_no_results() {
         // arrange: read out of the bounds where attributes of interest exist
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -352,6 +385,7 @@
                 vec![4, 5, 6],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByGroupTypeRequest {
@@ -359,7 +393,11 @@
             ending_handle: AttHandle(6).into(),
             attribute_group_type: PRIMARY_SERVICE_DECLARATION_UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_group_type_request(att_view, 31, &db));
+        let response = tokio_test::block_on(handle_read_by_group_type_request(
+            att_view,
+            31,
+            &client.downgrade(),
+        ));
 
         // assert: we return ATTRIBUTE_NOT_FOUND
         assert_eq!(
diff --git a/system/rust/src/gatt/server/transactions/read_by_type_request.rs b/system/rust/src/gatt/server/transactions/read_by_type_request.rs
index eec8815..8858609 100644
--- a/system/rust/src/gatt/server/transactions/read_by_type_request.rs
+++ b/system/rust/src/gatt/server/transactions/read_by_type_request.rs
@@ -1,5 +1,5 @@
 use crate::core::uuid::Uuid;
-use crate::gatt::server::att_database::StableAttDatabase;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att::{self, AttErrorCode};
 use pdl_runtime::EncodeError;
 
@@ -12,7 +12,7 @@
 pub async fn handle_read_by_type_request(
     request: att::AttReadByTypeRequest,
     mtu: usize,
-    db: &impl StableAttDatabase,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     // As per spec (5.3 Vol 3F 3.4.4.1)
     // > If an attribute in the set of requested attributes would cause an
@@ -34,10 +34,11 @@
         return failure_response.try_into();
     };
 
+    let all_attrs = client.list_attributes();
     let Some(attrs) = filter_to_range(
         request.starting_handle.into(),
         request.ending_handle.into(),
-        db.list_attributes().into_iter(),
+        all_attrs.iter(),
     ) else {
         failure_response.error_code = AttErrorCode::InvalidHandle;
         return failure_response.try_into();
@@ -47,7 +48,7 @@
     let mut out = PayloadAccumulator::new(mtu - 2);
 
     // MTU-4 limit comes from Core Spec 5.3 Vol 3F 3.4.4.1
-    match filter_read_attributes_by_size_type(db, attrs, request_type, mtu - 4).await {
+    match filter_read_attributes_by_size_type(client, attrs, request_type, mtu - 4).await {
         Ok(attrs) => {
             for AttributeWithValue { attr, value } in attrs {
                 if !out.push(att::AttReadByTypeDataElement { handle: attr.handle.into(), value }) {
@@ -73,19 +74,21 @@
     use super::*;
 
     use crate::core::uuid::Uuid;
-    use crate::gatt::ids::AttHandle;
+    use crate::gatt::ids::{AttHandle, TransportIndex};
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::AttAttribute;
     use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
 
     const UUID: Uuid = Uuid::new(1234);
     const ANOTHER_UUID: Uuid = Uuid::new(2345);
+    const TCB_IDX: TransportIndex = TransportIndex(1);
 
     #[test]
     fn test_single_matching_attr() {
         // arrange
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(3),
                 type_: UUID,
@@ -93,6 +96,7 @@
             },
             vec![4, 5],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByTypeRequest {
@@ -100,7 +104,8 @@
             ending_handle: AttHandle(6).into(),
             attribute_type: UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_type_request(att_view, 31, &db));
+        let response =
+            tokio_test::block_on(handle_read_by_type_request(att_view, 31, &client.downgrade()));
 
         // assert
         assert_eq!(
@@ -118,7 +123,7 @@
     #[test]
     fn test_type_filtering() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -144,6 +149,7 @@
                 vec![6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByTypeRequest {
@@ -151,7 +157,8 @@
             ending_handle: AttHandle(6).into(),
             attribute_type: UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_type_request(att_view, 31, &db));
+        let response =
+            tokio_test::block_on(handle_read_by_type_request(att_view, 31, &client.downgrade()));
 
         // assert: we correctly filtered by type (so we are using the filter_by_type
         // utility)
@@ -176,7 +183,7 @@
     #[test]
     fn test_limit_total_size() {
         // arrange
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -194,6 +201,7 @@
                 vec![5, 6, 7],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: read with MTU = 8, so we can only fit the first attribute (untruncated)
         let att_view = att::AttReadByTypeRequest {
@@ -201,7 +209,8 @@
             ending_handle: AttHandle(6).into(),
             attribute_type: UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_type_request(att_view, 8, &db));
+        let response =
+            tokio_test::block_on(handle_read_by_type_request(att_view, 8, &client.downgrade()));
 
         // assert: we return only the first attribute
         assert_eq!(
@@ -219,7 +228,7 @@
     #[test]
     fn test_no_results() {
         // arrange: read out of the bounds where attributes of interest exist
-        let db = TestAttDatabase::new(vec![
+        let db = new_test_database(vec![
             (
                 AttAttribute {
                     handle: AttHandle(3),
@@ -237,6 +246,7 @@
                 vec![4, 5, 6],
             ),
         ]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByTypeRequest {
@@ -244,7 +254,8 @@
             ending_handle: AttHandle(6).into(),
             attribute_type: UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_type_request(att_view, 31, &db));
+        let response =
+            tokio_test::block_on(handle_read_by_type_request(att_view, 31, &client.downgrade()));
 
         // assert: we return ATTRIBUTE_NOT_FOUND
         assert_eq!(
@@ -261,7 +272,8 @@
     #[test]
     fn test_range_validation() {
         // arrange: put a non-readable attribute in the db with the right type
-        let db = TestAttDatabase::new(vec![]);
+        let db = new_test_database(vec![]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
         let att_view = att::AttReadByTypeRequest {
@@ -269,7 +281,8 @@
             ending_handle: AttHandle(6).into(),
             attribute_type: UUID.into(),
         };
-        let response = tokio_test::block_on(handle_read_by_type_request(att_view, 31, &db));
+        let response =
+            tokio_test::block_on(handle_read_by_type_request(att_view, 31, &client.downgrade()));
 
         // assert: we return an INVALID_HANDLE error
         assert_eq!(
diff --git a/system/rust/src/gatt/server/transactions/read_request.rs b/system/rust/src/gatt/server/transactions/read_request.rs
index 32d4e48..2d0fb45 100644
--- a/system/rust/src/gatt/server/transactions/read_request.rs
+++ b/system/rust/src/gatt/server/transactions/read_request.rs
@@ -1,15 +1,15 @@
-use crate::gatt::server::att_database::AttDatabase;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att;
 use pdl_runtime::EncodeError;
 
-pub async fn handle_read_request<T: AttDatabase>(
+pub async fn handle_read_request(
     request: att::AttReadRequest,
     mtu: usize,
-    db: &T,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     let handle = request.attribute_handle.into();
 
-    match db.read_attribute(handle).await {
+    match client.read_attribute(handle).await {
         Ok(mut data) => {
             // as per 5.3 3F 3.4.4.4 ATT_READ_RSP, we truncate to MTU - 1
             data.truncate(mtu - 1);
@@ -24,14 +24,14 @@
     }
 }
 
-pub async fn handle_read_blob_request<T: AttDatabase>(
+pub async fn handle_read_blob_request(
     request: att::AttReadBlobRequest,
     mtu: usize,
-    db: &T,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     let handle = request.attribute_handle.into();
 
-    match db.read_attribute(handle).await {
+    match client.read_attribute(handle).await {
         Ok(data) => {
             let offset = request.offset as usize;
             if offset > data.len() {
@@ -57,10 +57,10 @@
     }
 }
 
-pub async fn handle_read_multiple_variable_request<T: AttDatabase>(
+pub async fn handle_read_multiple_variable_request(
     request: att::AttReadMultipleVariableRequest,
     mtu: usize,
-    db: &T,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     if request.attribute_handles.len() < 2 {
         return att::AttErrorResponse {
@@ -75,7 +75,7 @@
     };
     let mut space = mtu - 1; // -1 for op code.
     for handle in request.attribute_handles {
-        match db.read_attribute(handle.clone().into()).await {
+        match client.read_attribute(handle.clone().into()).await {
             Ok(data) => {
                 if space >= 2 {
                     let amount = std::cmp::min(data.len(), space - 2);
@@ -111,14 +111,19 @@
 mod test {
     use super::*;
 
+    use crate::core::shared_box::SharedBox;
     use crate::core::uuid::Uuid;
-    use crate::gatt::ids::AttHandle;
+    use crate::gatt::ids::{AttHandle, TransportIndex};
+    use crate::gatt::server::att_client::AttClient;
     use crate::gatt::server::att_database::{AttAttribute, AttPermissions};
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::gatt_database::GattDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
 
-    fn make_db_with_handle_and_value(handle: u16, value: Vec<u8>) -> TestAttDatabase {
-        TestAttDatabase::new(vec![(
+    const TCB_IDX: TransportIndex = TransportIndex(1);
+
+    fn make_db_with_handle_and_value(handle: u16, value: Vec<u8>) -> SharedBox<GattDatabase> {
+        new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(handle),
                 type_: Uuid::new(0x1234),
@@ -131,17 +136,18 @@
     fn do_read_request_with_handle_and_mtu(
         handle: u16,
         mtu: usize,
-        db: &TestAttDatabase,
+        client: &WeakAttClient,
     ) -> Result<att::Att, EncodeError> {
         let att_view = att::AttReadRequest { attribute_handle: AttHandle(handle).into() };
-        tokio_test::block_on(handle_read_request(att_view, mtu, db))
+        tokio_test::block_on(handle_read_request(att_view, mtu, client))
     }
 
     #[test]
     fn test_simple_read() {
         let db = make_db_with_handle_and_value(3, vec![4, 5]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
-        let response = do_read_request_with_handle_and_mtu(3, 31, &db);
+        let response = do_read_request_with_handle_and_mtu(3, 31, &client.downgrade());
 
         assert_eq!(response, att::AttReadResponse { value: vec![4, 5] }.try_into());
     }
@@ -149,9 +155,10 @@
     #[test]
     fn test_truncated_read() {
         let db = make_db_with_handle_and_value(3, vec![4, 5]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = do_read_request_with_handle_and_mtu(3, 2, &db);
+        let response = do_read_request_with_handle_and_mtu(3, 2, &client.downgrade());
 
         // assert
         assert_eq!(response, att::AttReadResponse { value: vec![4] }.try_into());
@@ -160,9 +167,10 @@
     #[test]
     fn test_missed_read() {
         let db = make_db_with_handle_and_value(3, vec![4, 5]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = do_read_request_with_handle_and_mtu(4, 31, &db);
+        let response = do_read_request_with_handle_and_mtu(4, 31, &client.downgrade());
 
         // assert
         assert_eq!(
@@ -176,8 +184,8 @@
         );
     }
 
-    fn make_db_with_unreadable_handle(handle: u16) -> TestAttDatabase {
-        TestAttDatabase::new(vec![(
+    fn make_db_with_unreadable_handle(handle: u16) -> SharedBox<GattDatabase> {
+        new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(handle),
                 type_: Uuid::new(0x1234),
@@ -190,9 +198,10 @@
     #[test]
     fn test_not_readable() {
         let db = make_db_with_unreadable_handle(3);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act
-        let response = do_read_request_with_handle_and_mtu(3, 31, &db);
+        let response = do_read_request_with_handle_and_mtu(3, 31, &client.downgrade());
 
         // assert
         assert_eq!(
diff --git a/system/rust/src/gatt/server/transactions/write_request.rs b/system/rust/src/gatt/server/transactions/write_request.rs
index f3a41e6..c225536 100644
--- a/system/rust/src/gatt/server/transactions/write_request.rs
+++ b/system/rust/src/gatt/server/transactions/write_request.rs
@@ -1,14 +1,15 @@
-use crate::gatt::server::att_database::AttDatabase;
+use crate::gatt::callbacks::TransactionDecision;
+use crate::gatt::server::att_client::WeakAttClient;
 use crate::packets::att;
 use pdl_runtime::EncodeError;
 
-pub async fn handle_write_request<T: AttDatabase>(
+pub async fn handle_write_request(
     request: att::AttWriteRequest,
-    db: &T,
+    client: &WeakAttClient,
 ) -> Result<att::Att, EncodeError> {
     let handle = request.handle.into();
     let value = request.value;
-    match db.write_attribute(handle, &value).await {
+    match client.write_attribute(handle, &value).await {
         Ok(()) => att::AttWriteResponse {}.try_into(),
         Err(error_code) => att::AttErrorResponse {
             opcode_in_error: att::AttOpcode::WriteRequest,
@@ -19,6 +20,44 @@
     }
 }
 
+pub async fn handle_prepare_write_request(
+    request: att::AttPrepareWriteRequest,
+    client: &WeakAttClient,
+) -> Result<att::Att, EncodeError> {
+    let att::AttPrepareWriteRequest { handle, offset, value } = request;
+    match client.prepare_write_attribute(handle.clone().into(), offset as u32, &value).await {
+        Ok(()) => att::AttPrepareWriteResponse { handle, offset, value }.try_into(),
+        Err(error_code) => att::AttErrorResponse {
+            opcode_in_error: att::AttOpcode::PrepareWriteRequest,
+            handle_in_error: handle,
+            error_code,
+        }
+        .try_into(),
+    }
+}
+
+pub async fn handle_execute_write_request(
+    request: att::AttExecuteWriteRequest,
+    client: &WeakAttClient,
+) -> Result<att::Att, EncodeError> {
+    match client
+        .execute(if request.commit & 1 == 1 {
+            TransactionDecision::Execute
+        } else {
+            TransactionDecision::Cancel
+        })
+        .await
+    {
+        Ok(()) => att::AttExecuteWriteResponse {}.try_into(),
+        Err((handle, error_code)) => att::AttErrorResponse {
+            opcode_in_error: att::AttOpcode::ExecuteWriteRequest,
+            handle_in_error: handle.into(),
+            error_code,
+        }
+        .try_into(),
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
@@ -26,16 +65,19 @@
     use tokio_test::block_on;
 
     use crate::core::uuid::Uuid;
-    use crate::gatt::ids::AttHandle;
-    use crate::gatt::server::att_database::{AttAttribute, AttDatabase};
+    use crate::gatt::ids::{AttHandle, TransportIndex};
+    use crate::gatt::server::att_client::AttClient;
+    use crate::gatt::server::att_database::AttAttribute;
     use crate::gatt::server::gatt_database::AttPermissions;
-    use crate::gatt::server::test::test_att_db::TestAttDatabase;
+    use crate::gatt::server::test::test_att_db::new_test_database;
     use crate::packets::att;
 
+    const TCB_IDX: TransportIndex = TransportIndex(1);
+
     #[test]
     fn test_successful_write() {
         // arrange: db with one writable attribute
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(1),
                 type_: Uuid::new(0x1234),
@@ -44,20 +86,21 @@
             vec![],
         )]);
         let data = vec![1, 2];
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
 
         // act: write to the attribute
         let att_view = att::AttWriteRequest { handle: AttHandle(1).into(), value: data.clone() };
-        let resp = block_on(handle_write_request(att_view, &db));
+        let resp = block_on(handle_write_request(att_view, &client.downgrade()));
 
         // assert: that the write succeeded
         assert_eq!(resp, att::AttWriteResponse {}.try_into());
-        assert_eq!(block_on(db.read_attribute(AttHandle(1))).unwrap(), data);
+        assert_eq!(block_on(client.read_attribute(AttHandle(1))).unwrap(), data);
     }
 
     #[test]
     fn test_failed_write() {
         // arrange: db with no writable attributes
-        let db = TestAttDatabase::new(vec![(
+        let db = new_test_database(vec![(
             AttAttribute {
                 handle: AttHandle(1),
                 type_: Uuid::new(0x1234),
@@ -65,9 +108,10 @@
             },
             vec![],
         )]);
+        let (client, _) = AttClient::new_test_client(TCB_IDX, &db);
         // act: write to the attribute
         let att_view = att::AttWriteRequest { handle: AttHandle(1).into(), value: vec![1, 2] };
-        let resp = block_on(handle_write_request(att_view, &db));
+        let resp = block_on(handle_write_request(att_view, &client.downgrade()));
 
         // assert: that the write failed
         assert_eq!(
diff --git a/system/rust/src/packets.pdl b/system/rust/src/packets.pdl
index e15a054..7168e65 100644
--- a/system/rust/src/packets.pdl
+++ b/system/rust/src/packets.pdl
@@ -65,6 +65,7 @@
   INVALID_OFFSET = 0x07,
   ATTRIBUTE_NOT_FOUND = 0x0A,
   ATTRIBUTE_NOT_LONG = 0x0B,
+  INVALID_ATTRIBUTE_VALUE_LENGTH = 0x0D,
   UNLIKELY_ERROR = 0x0E,
   UNSUPPORTED_GROUP_TYPE = 0x10,
   APPLICATION_ERROR = 0x80,
@@ -251,6 +252,25 @@
 
 packet AttWriteResponse : Att(opcode = WRITE_RESPONSE) {}
 
+packet AttPrepareWriteRequest : Att(opcode = PREPARE_WRITE_REQUEST) {
+  handle : AttHandle,
+  offset : 16,
+  value : 8[],
+}
+
+packet AttPrepareWriteResponse : Att(opcode = PREPARE_WRITE_RESPONSE) {
+  handle : AttHandle,
+  offset : 16,
+  value : 8[],
+}
+
+packet AttExecuteWriteRequest : Att(opcode = EXECUTE_WRITE_REQUEST) {
+  commit : 1, // true = commit, false = cancel
+  _reserved_ : 7,
+}
+
+packet AttExecuteWriteResponse : Att(opcode = EXECUTE_WRITE_RESPONSE) {}
+
 packet AttErrorResponse : Att(opcode = ERROR_RESPONSE) {
   opcode_in_error: AttOpcode,
   handle_in_error: AttHandle,
diff --git a/system/stack/rfcomm/rfc_port_fsm.cc b/system/stack/rfcomm/rfc_port_fsm.cc
index dec82b6..c2e32f6 100644
--- a/system/stack/rfcomm/rfc_port_fsm.cc
+++ b/system/stack/rfcomm/rfc_port_fsm.cc
@@ -576,17 +576,15 @@
 
     case RFC_PORT_EVENT_DM:
       log::warn("RFC_EVENT_DM|RFC_EVENT_UA[{}], port_handle:{}", event, p_port->handle);
-      if (com::android::bluetooth::flags::rfcomm_always_disc_initiator_in_disc_wait_ua()) {
-        // If we got a DM in RFC_STATE_DISC_WAIT_UA, it's likely that both ends
-        // attempt to DISC at the same time and both get a DM.
-        // Without setting this flag the both ends would start the same timers,
-        // wait, and still DISC the multiplexer at the same time eventually.
-        // The wait is meaningless and would block all other services that rely
-        // on RFCOMM such as HFP.
-        // Thus, setting this flag here to save us a timeout and doesn't
-        // introduce further RFCOMM event changes.
-        p_port->rfc.p_mcb->is_disc_initiator = true;
-      }
+      // If we got a DM in RFC_STATE_DISC_WAIT_UA, it's likely that both ends
+      // attempt to DISC at the same time and both get a DM.
+      // Without setting this flag the both ends would start the same timers,
+      // wait, and still DISC the multiplexer at the same time eventually.
+      // The wait is meaningless and would block all other services that rely
+      // on RFCOMM such as HFP.
+      // Thus, setting this flag here to save us a timeout and doesn't
+      // introduce further RFCOMM event changes.
+      p_port->rfc.p_mcb->is_disc_initiator = true;
       rfc_port_closed(p_port);
       return;