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

Commit ae384103 authored by Sal Savage's avatar Sal Savage
Browse files

Storage Refactor: Introduce PbapClientObexClient and others

This change introduces an OBEX client object that no longer has the
responsibility of storing contacts once they're downloaded. Instead, it
funnels contacts back to its owner via a callback.

This object will replace PbapClientConnectionHandler.

Bug: 365626536
Bug: 376461939
Test: atest com.android.bluetooth.pbapclient
Test: m com.android.btservices
Change-Id: If254e644c479d870afe22b9cd574d4f883998c98
parent 00affc8e
Loading
Loading
Loading
Loading
+674 −0

File added.

Preview size limit exceeded, changes collapsed.

+118 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

package com.android.bluetooth.pbapclient;

import android.bluetooth.BluetoothSocket;

import com.android.bluetooth.Utils;
import com.android.obex.ObexTransport;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;

/** Generic Obex Transport class, to be used in OBEX based Bluetooth Profiles. */
public class PbapClientObexTransport implements ObexTransport {
    private final PbapClientSocket mSocket;

    /** Will default at the maximum packet length. */
    public static final int PACKET_SIZE_UNSPECIFIED = -1;

    private int mMaxTransmitPacketSize = PACKET_SIZE_UNSPECIFIED;
    private int mMaxReceivePacketSize = PACKET_SIZE_UNSPECIFIED;

    public PbapClientObexTransport(PbapClientSocket socket) {
        mSocket = Objects.requireNonNull(socket);
    }

    @Override
    public void close() throws IOException {
        mSocket.close();
    }

    @Override
    public DataInputStream openDataInputStream() throws IOException {
        return new DataInputStream(openInputStream());
    }

    @Override
    public DataOutputStream openDataOutputStream() throws IOException {
        return new DataOutputStream(openOutputStream());
    }

    @Override
    public InputStream openInputStream() throws IOException {
        return mSocket.getInputStream();
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
        return mSocket.getOutputStream();
    }

    @Override
    public void connect() throws IOException {}

    @Override
    public void create() throws IOException {}

    @Override
    public void disconnect() throws IOException {}

    @Override
    public void listen() throws IOException {}

    /** Returns true if this transport is connected */
    public boolean isConnected() throws IOException {
        return true;
    }

    @Override
    public int getMaxTransmitPacketSize() {
        if (mSocket.getConnectionType() != BluetoothSocket.TYPE_L2CAP) {
            return mMaxTransmitPacketSize;
        }
        return mSocket.getMaxTransmitPacketSize();
    }

    @Override
    public int getMaxReceivePacketSize() {
        if (mSocket.getConnectionType() != BluetoothSocket.TYPE_L2CAP) {
            return mMaxReceivePacketSize;
        }
        return mSocket.getMaxReceivePacketSize();
    }

    /** Get the remote device MAC address associated with the transport, as a tring */
    public String getRemoteAddress() {
        String identityAddress = Utils.getBrEdrAddress(mSocket.getRemoteDevice());
        return mSocket.getConnectionType() == BluetoothSocket.TYPE_RFCOMM
                ? identityAddress
                : mSocket.getRemoteDevice().getAddress();
    }

    @Override
    public boolean isSrmSupported() {
        if (mSocket.getConnectionType() == BluetoothSocket.TYPE_L2CAP) {
            return true;
        }
        return false;
    }
}
+188 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

package com.android.bluetooth.pbapclient;

import static android.Manifest.permission.BLUETOOTH_CONNECT;

import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;

import com.android.internal.annotations.VisibleForTesting;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;

/**
 * A testable object that wraps BluetoothSocket objects
 *
 * <p>To inject input and output streams in place of an underlying L2CAP or RFCOMM connection, use
 * the inject(InputStream input, OutputStream output) function. All sockets created after injecting
 * streams will use the injected streams instead.
 */
public class PbapClientSocket {
    private static final String TAG = PbapClientSocket.class.getSimpleName();

    // Houses the streams to be injected in place of the underlying BluetoothSocket
    private static InputStream sInjectedInput;
    private static OutputStream sInjectedOutput;

    // Houses the actual socket if used
    private final BluetoothSocket mSocket;

    // Houses injected streams, if used for this object
    private final BluetoothDevice mDevice;
    private final int mType;
    private final InputStream mInjectedInput;
    private final OutputStream mInjectedOutput;

    @VisibleForTesting
    static void inject(InputStream input, OutputStream output) {
        sInjectedInput = Objects.requireNonNull(input);
        sInjectedOutput = Objects.requireNonNull(output);
    }

    /** A static utility to create an L2CAP based socket for a given device */
    public static PbapClientSocket getL2capSocketForDevice(BluetoothDevice device, int psm)
            throws IOException {
        if (sInjectedInput != null || sInjectedOutput != null) {
            return new PbapClientSocket(
                    device, BluetoothSocket.TYPE_L2CAP, sInjectedInput, sInjectedOutput);
        }

        BluetoothSocket socket = device.createL2capSocket(psm);
        return new PbapClientSocket(socket);
    }

    /** A static utility to create an RFCOMM based socket for a given device */
    public static PbapClientSocket getRfcommSocketForDevice(BluetoothDevice device, int channel)
            throws IOException {
        if (sInjectedInput != null || sInjectedOutput != null) {
            return new PbapClientSocket(
                    device, BluetoothSocket.TYPE_RFCOMM, sInjectedInput, sInjectedOutput);
        }
        BluetoothSocket socket = device.createRfcommSocket(channel);
        return new PbapClientSocket(socket);
    }

    private PbapClientSocket(BluetoothSocket socket) {
        mSocket = socket;

        mDevice = null;
        mType = -1;
        mInjectedInput = null;
        mInjectedOutput = null;
    }

    private PbapClientSocket(
            BluetoothDevice device, int type, InputStream input, OutputStream output) {
        mSocket = null;

        mDevice = device;
        mType = type;
        mInjectedInput = input;
        mInjectedOutput = output;
    }

    /** Invokes the underlying BluetoothSocket#connect(), or does nothing if a socket is injected */
    @RequiresPermission(BLUETOOTH_CONNECT)
    public void connect() throws IOException {
        if (mSocket != null) {
            mSocket.connect();
        }
    }

    /** Invokes the underlying BluetoothSocket#getRemoteDevice(), or the injected device */
    public BluetoothDevice getRemoteDevice() {
        if (mSocket != null) {
            return mSocket.getRemoteDevice();
        }
        return mDevice;
    }

    /** Invokes the underlying BluetoothSocket#getConnectionType(), or the injected type */
    public int getConnectionType() {
        if (mSocket != null) {
            return mSocket.getConnectionType();
        }
        return mType;
    }

    /**
     * Invokes the underlying BluetoothSocket#getMaxTransmitPacketSize(), or returns the spec
     * minimum 255 when a socket is injected
     */
    public int getMaxTransmitPacketSize() {
        if (mSocket != null) {
            return mSocket.getMaxTransmitPacketSize();
        }
        return 255; // Minimum by specification
    }

    /**
     * Invokes the underlying BluetoothSocket#getMaxReceivePacketSize(), or returns the spec minimum
     * 255 when a socket is injected
     */
    public int getMaxReceivePacketSize() {
        if (mSocket != null) {
            return mSocket.getMaxReceivePacketSize();
        }
        return 255; // Minimum by specification
    }

    /** Invokes the underlying BluetoothSocket#getInputStream(), or returns the injected input */
    public InputStream getInputStream() throws IOException {
        if (mInjectedInput != null) {
            return mInjectedInput;
        }
        return mSocket.getInputStream();
    }

    /** Invokes the underlying BluetoothSocket#getOutputStream(), or returns the injected output */
    public OutputStream getOutputStream() throws IOException {
        if (mInjectedOutput != null) {
            return mInjectedOutput;
        }
        return mSocket.getOutputStream();
    }

    /** Invokes the underlying BluetoothSocket#close(), or the injected sockets's close() */
    public void close() throws IOException {
        if (mSocket != null) {
            mSocket.close();
            return;
        }

        if (sInjectedInput != null) {
            sInjectedInput.close();
        }

        if (mInjectedOutput != null) {
            mInjectedOutput.close();
        }
    }

    @Override
    public String toString() {
        if (mSocket != null) {
            return mSocket.toString();
        }
        return "FakeBluetoothSocket";
    }
}
+691 −0

File added.

Preview size limit exceeded, changes collapsed.

+208 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.
 */

package com.android.bluetooth.pbapclient;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;

import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.TestUtils;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RunWith(AndroidJUnit4.class)
public class PbapClientObexTransportTest {
    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

    private BluetoothAdapter mAdapter;
    private BluetoothDevice mTestDevice;

    @Mock private PbapClientSocket mMockSocket;
    @Mock private InputStream mMockInputStream;
    @Mock private OutputStream mMockOutputStream;

    @Before
    public void setUp() throws IOException {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mTestDevice = TestUtils.getTestDevice(mAdapter, 1);

        doReturn(mMockInputStream).when(mMockSocket).getInputStream();
        doReturn(mMockOutputStream).when(mMockSocket).getOutputStream();
        doReturn(BluetoothSocket.TYPE_L2CAP).when(mMockSocket).getConnectionType();
        doReturn(255).when(mMockSocket).getMaxTransmitPacketSize();
        doReturn(255).when(mMockSocket).getMaxReceivePacketSize();
        doReturn(mTestDevice).when(mMockSocket).getRemoteDevice();
    }

    @Test
    public void testCloseTransport_socketIsClosed() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        transport.close();
        verify(mMockSocket).close();
    }

    @Test
    public void testOpenDataInputStream_containsSocketStream() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        DataInputStream in = transport.openDataInputStream();

        // DataInputStreams don't allow access to their underlying object, so we
        // can just do an operation and make sure the mock stream is used for
        // said operation
        doReturn(1).when(mMockInputStream).read();
        in.readBoolean();
        verify(mMockInputStream).read();
    }

    @Test
    public void testOpenDataOutputStream_containsSocketStream() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        DataOutputStream out = transport.openDataOutputStream();

        // DataOutputStreams don't allow access to their underlying object, so we
        // can just do an operation and make sure the mock stream is used for
        // said operation
        out.flush();
        verify(mMockOutputStream).flush();
    }

    @Test
    public void testOpenInputStream_containsSocketStream() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        InputStream in = transport.openInputStream();
        assertThat(mMockInputStream).isEqualTo(in);
    }

    @Test
    public void testOpenOutputStream_containsSocketStream() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        OutputStream out = transport.openOutputStream();
        assertThat(mMockOutputStream).isEqualTo(out);
    }

    @Test
    public void testConnect_doesNothing() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        transport.connect();
        verifyNoMoreInteractions(mMockSocket);
    }

    @Test
    public void testCreate_doesNothing() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        transport.create();
        verifyNoMoreInteractions(mMockSocket);
    }

    @Test
    public void testDisconnect_doesNothing() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        transport.disconnect();
        verifyNoMoreInteractions(mMockSocket);
    }

    @Test
    public void testListen_doesNothing() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        transport.listen();
        verifyNoMoreInteractions(mMockSocket);
    }

    @Test
    public void testIsConnected_returnsTrue() throws IOException {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.isConnected()).isTrue();
        verifyNoMoreInteractions(mMockSocket);
    }

    @Test
    public void testGetMaxTransmitPacketSize_transportL2cap_returns255() {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.getMaxTransmitPacketSize()).isEqualTo(255);
    }

    @Test
    public void testGetMaxTransmitPacketSize_transportRfcomm_returnsUnspecified() {
        doReturn(BluetoothSocket.TYPE_RFCOMM).when(mMockSocket).getConnectionType();
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.getMaxTransmitPacketSize())
                .isEqualTo(PbapClientObexTransport.PACKET_SIZE_UNSPECIFIED);
    }

    @Test
    public void testGetMaxReceivePacketSize_transportL2cap_returns255() {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.getMaxReceivePacketSize()).isEqualTo(255);
    }

    @Test
    public void testGetMaxReceivePacketSize_transportRfcomm_returnsUnspecified() {
        doReturn(BluetoothSocket.TYPE_RFCOMM).when(mMockSocket).getConnectionType();
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.getMaxReceivePacketSize())
                .isEqualTo(PbapClientObexTransport.PACKET_SIZE_UNSPECIFIED);
    }

    @Test
    public void testGetRemoteAddress_transportL2cap_returnsDeviceAddress() {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.getRemoteAddress()).isEqualTo(mTestDevice.getAddress());
    }

    @Test
    public void testGetRemoteAddress_transportRfcomm_returnsDeviceIdentityAddress() {
        doReturn(BluetoothSocket.TYPE_RFCOMM).when(mMockSocket).getConnectionType();
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        // See "Flags.identityAddressNullIfNotKnown():"
        // Identity address won't be "known" by the stack for a test device, so it'll return null.
        // assertThat(transport.getRemoteAddress()).isNull();
        assertThat(transport.getRemoteAddress()).isEqualTo(mTestDevice.getAddress());
    }

    @Test
    public void testIsSrmSupported_transportL2cap_returnsTrue() {
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.isSrmSupported()).isTrue();
    }

    @Test
    public void testIsSrmSupported_transportRfcomm_returnsFalse() {
        doReturn(BluetoothSocket.TYPE_RFCOMM).when(mMockSocket).getConnectionType();
        PbapClientObexTransport transport = new PbapClientObexTransport(mMockSocket);
        assertThat(transport.isSrmSupported()).isFalse();
    }
}
Loading