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

Commit e3276833 authored by William Escande's avatar William Escande Committed by Gerrit Code Review
Browse files

Merge "GAP: Allow BT name upto 248 bytes from setName" into main

parents bdeeb88a e1c6797b
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));
    }
}