Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e1c6797b authored by William Escande's avatar William Escande
Browse files

GAP: Allow BT name upto 248 bytes from setName

As per BT spec BT name should be of max 248 bytes but setname api allows
more than 248 bytes in bt name and bt name in stack and app out of sync.

Hence allow only 248 bytes from setName.

Bug: 296517746
Test: atest BluetoothInstrumentationTests:AdapterPropertiesTest
Test: atest BluetoothInstrumentationTests:UtilsTest
Change-Id: Id6c3f1bbac2f71ac7ef0544305c92905e71ddc1e
parent 63384dde
Loading
Loading
Loading
Loading
+43 −1
Original line number Diff line number Diff line
@@ -63,7 +63,6 @@ import android.provider.DeviceConfig;
import android.provider.Telephony;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
@@ -1235,4 +1234,47 @@ public final class Utils {
        return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
                || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    }

    /**
     * Returns the longest prefix of a string for which the UTF-8 encoding fits into the given
     * number of bytes, with the additional guarantee that the string is not truncated in the middle
     * of a valid surrogate pair.
     *
     * <p>Unpaired surrogates are counted as taking 3 bytes of storage. However, a subsequent
     * attempt to actually encode a string containing unpaired surrogates is likely to be rejected
     * by the UTF-8 implementation.
     *
     * <p>(copied from framework/base/core/java/android/text/TextUtils.java)
     *
     * @param str a string
     * @param maxbytes the maximum number of UTF-8 encoded bytes
     * @return the beginning of the string, so that it uses at most maxbytes bytes in UTF-8
     * @throws IndexOutOfBoundsException if maxbytes is negative
     */
    public static String truncateStringForUtf8Storage(String str, int maxbytes) {
        if (maxbytes < 0) {
            throw new IndexOutOfBoundsException();
        }

        int bytes = 0;
        for (int i = 0, len = str.length(); i < len; i++) {
            char c = str.charAt(i);
            if (c < 0x80) {
                bytes += 1;
            } else if (c < 0x800) {
                bytes += 2;
            } else if (c < Character.MIN_SURROGATE
                    || c > Character.MAX_SURROGATE
                    || str.codePointAt(i) < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
                bytes += 3;
            } else {
                bytes += 4;
                i += (bytes > maxbytes) ? 0 : 1;
            }
            if (bytes > maxbytes) {
                return str.substring(0, i);
            }
        }
        return str;
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -82,6 +82,7 @@ class AdapterProperties {
            "persist.bluetooth.a2dp_offload.disabled";

    private static final long DEFAULT_DISCOVERY_TIMEOUT_MS = 12800;
    @VisibleForTesting static final int BLUETOOTH_NAME_MAX_LENGTH_BYTES = 248;
    private static final int BD_ADDR_LEN = 6; // in bytes

    private volatile String mName;
@@ -318,7 +319,11 @@ class AdapterProperties {
    boolean setName(String name) {
        synchronized (mObject) {
            return mService.getNative()
                    .setAdapterProperty(AbstractionLayer.BT_PROPERTY_BDNAME, name.getBytes());
                    .setAdapterProperty(
                            AbstractionLayer.BT_PROPERTY_BDNAME,
                            Utils.truncateStringForUtf8Storage(
                                            name, BLUETOOTH_NAME_MAX_LENGTH_BYTES)
                                    .getBytes());
        }
    }

+70 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

/**
@@ -249,4 +250,73 @@ public class UtilsTest {
        doThrow(new IOException()).when(os).close();
        Utils.safeCloseStream(os);
    }

    @Test
    public void truncateUtf8_toZeroLength_isEmpty() {
        assertThat(Utils.truncateStringForUtf8Storage("abc", 0)).isEmpty();
    }

    @Test
    public void truncateUtf8_longCase_isExpectedResult() {
        StringBuilder builder = new StringBuilder();

        int n = 50;
        for (int i = 0; i < 2 * n; i++) {
            builder.append("哈");
        }
        String initial = builder.toString();
        String result = Utils.truncateStringForUtf8Storage(initial, n);

        // Result should be the beginning of initial
        assertThat(initial.startsWith(result)).isTrue();

        // Result should take less than n bytes in UTF-8
        assertThat(result.getBytes(StandardCharsets.UTF_8).length).isAtMost(n);

        // result + the next codePoint should take strictly more than
        // n bytes in UTF-8
        assertThat(
                        initial.substring(0, initial.offsetByCodePoints(result.length(), 1))
                                .getBytes(StandardCharsets.UTF_8)
                                .length)
                .isGreaterThan(n);
    }

    @Test
    public void truncateUtf8_untruncatedString_isEqual() {
        String s = "sf\u20ACgk\u00E9ls\u00E9fg";
        assertThat(Utils.truncateStringForUtf8Storage(s, 100)).isEqualTo(s);
    }

    @Test
    public void truncateUtf8_inMiddleOfSurrogate_isStillUtf8() {
        StringBuilder builder = new StringBuilder();
        String beginning = "a";
        builder.append(beginning);
        builder.append(Character.toChars(0x1D11E));

        // \u1D11E is a surrogate and needs 4 bytes in UTF-8. beginning == "a" uses
        // only 1 bytes in UTF8
        // As we allow only 3 bytes for the whole string, so just 2 for this
        // codePoint, there is not enough place and the string will be truncated
        // just before it
        assertThat(Utils.truncateStringForUtf8Storage(builder.toString(), 3)).isEqualTo(beginning);
    }

    @Test
    public void truncateUtf8_inMiddleOfChar_isStillUtf8() {
        StringBuilder builder = new StringBuilder();
        String beginning = "a";
        builder.append(beginning);
        builder.append(Character.toChars(0x20AC));

        // Like above, \u20AC uses 3 bytes in UTF-8, with "beginning", that makes
        // 4 bytes so it is too big and should be truncated
        assertThat(Utils.truncateStringForUtf8Storage(builder.toString(), 3)).isEqualTo(beginning);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void truncateUtf8_toNegativeSize_ThrowsException() {
        Utils.truncateStringForUtf8Storage("abc", -1);
    }
}
+48 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -37,6 +38,7 @@ import com.android.bluetooth.Utils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@@ -110,4 +112,50 @@ public class AdapterPropertiesTest {
        assertThat(mAdapterProperties.getBondedDevices()[0].getAddress())
                .isEqualTo(Utils.getAddressStringFromByte(TEST_BT_ADDR_BYTES_2));
    }

    @Test
    public void setName_shortName_isEqual() {
        StringBuilder builder = new StringBuilder();
        String stringName = "Wonderful Bluetooth Name Using utf8";
        builder.append(stringName);
        builder.append(Character.toChars(0x20AC));

        String initial = builder.toString();

        final ArgumentCaptor<byte[]> argumentName = ArgumentCaptor.forClass(byte[].class);

        mAdapterProperties.setName(initial);
        verify(mNativeInterface)
                .setAdapterProperty(
                        eq(AbstractionLayer.BT_PROPERTY_BDNAME), argumentName.capture());

        assertThat(argumentName.getValue()).isEqualTo(initial.getBytes());
    }

    @Test
    public void setName_tooLongName_isTruncated() {
        StringBuilder builder = new StringBuilder();
        String stringName = "Wonderful Bluetooth Name Using utf8 ... But this name is too long";
        builder.append(stringName);

        int n = 300;
        for (int i = 0; i < 2 * n; i++) {
            builder.append(Character.toChars(0x20AC));
        }

        String initial = builder.toString();

        final ArgumentCaptor<byte[]> argumentName = ArgumentCaptor.forClass(byte[].class);

        mAdapterProperties.setName(initial);
        verify(mNativeInterface)
                .setAdapterProperty(
                        eq(AbstractionLayer.BT_PROPERTY_BDNAME), argumentName.capture());

        byte[] name = argumentName.getValue();

        assertThat(name.length).isLessThan(initial.getBytes().length);

        assertThat(initial).startsWith(new String(name));
    }
}