blob: b618488f6fb05382d01dcbf1b55fe21bf49eabe6 [file] [log] [blame]
/*
* Copyright (C) 2023 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.car;
import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER;
import static android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT;
import static android.car.occupantconnection.CarOccupantConnectionManager.CONNECTION_ERROR_USER_REJECTED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.car.Car;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.occupantconnection.CarOccupantConnectionManager;
import android.car.occupantconnection.CarOccupantConnectionManager.ConnectionRequestCallback;
import android.car.occupantconnection.CarOccupantConnectionManager.PayloadCallback;
import android.car.occupantconnection.CarOccupantConnectionManager.PayloadTransferException;
import android.car.occupantconnection.ICarOccupantConnection;
import android.car.occupantconnection.IConnectionRequestCallback;
import android.car.occupantconnection.IPayloadCallback;
import android.car.occupantconnection.Payload;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.concurrent.Executor;
@RunWith(MockitoJUnitRunner.class)
public final class CarOccupantConnectionManagerUnitTest {
private static final String PACKAGE_NAME = "my_package_name";
private static final String RECEIVER_ENDPOINT_ID = "test_receiver_endpointId";
private static final int TIMEOUT_MS = 1000;
@Mock
private Car mCar;
@Mock
private Context mContext;
@Mock
private IBinder mBinder;
@Mock
private ICarOccupantConnection mService;
@Mock
private Executor mCallbackExecutor;
@Mock
private ConnectionRequestCallback mConnectionRequestCallback;
@Mock
private PayloadCallback mPayloadCallback;
private final OccupantZoneInfo mReceiverZone =
new OccupantZoneInfo(/* zoneId= */ 0, OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT);
private CarOccupantConnectionManager mOccupantConnectionManager;
@Before
public void setUp() {
when(mBinder.queryLocalInterface(anyString())).thenReturn(mService);
when(mCar.getContext()).thenReturn(mContext);
when(mContext.getPackageName()).thenReturn(PACKAGE_NAME);
mOccupantConnectionManager = new CarOccupantConnectionManager(mCar, mBinder);
}
@Test
public void testRequestConnectionWithNullParameters_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.requestConnection(
/* receiverZone= */ null, mCallbackExecutor, mConnectionRequestCallback));
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.requestConnection(
mReceiverZone, /* executor= */ null, mConnectionRequestCallback));
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.requestConnection(
mReceiverZone, mCallbackExecutor, /* callback= */ null));
}
@Test
public void testRequestConnectionWithPendingConnection_throwsException() {
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mock(ConnectionRequestCallback.class));
// The client shouldn't request another connection to the same occupant zone because there
// is a pending connection request to that occupant zone.
assertThrows(IllegalStateException.class,
() -> mOccupantConnectionManager.requestConnection(
mReceiverZone, mCallbackExecutor, mConnectionRequestCallback));
}
@Test
public void testRequestConnectionWithEstablishedConnection_throwsException()
throws RemoteException {
IConnectionRequestCallback[] binderCallback = new IConnectionRequestCallback[1];
doAnswer((invocation) -> {
Object[] args = invocation.getArguments();
binderCallback[0] = (IConnectionRequestCallback) args[2];
return null;
}).when(mService).requestConnection(anyString(), any(OccupantZoneInfo.class), any());
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mock(ConnectionRequestCallback.class));
binderCallback[0].onConnected(mReceiverZone);
// The client shouldn't request another connection to the same occupant zone because there
// is an established connection to that occupant zone.
assertThrows(IllegalStateException.class,
() -> mOccupantConnectionManager.requestConnection(
mReceiverZone, mCallbackExecutor, mConnectionRequestCallback));
}
@Test
public void testRequestConnectionWithRejectedConnection() throws RemoteException {
IConnectionRequestCallback[] binderCallback = new IConnectionRequestCallback[1];
doAnswer((invocation) -> {
Object[] args = invocation.getArguments();
binderCallback[0] = (IConnectionRequestCallback) args[2];
return null;
}).when(mService).requestConnection(anyString(), any(OccupantZoneInfo.class), any());
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mock(ConnectionRequestCallback.class));
binderCallback[0].onFailed(mReceiverZone, CONNECTION_ERROR_USER_REJECTED);
// The client can request another connection to the same occupant zone since the previous
// request was rejected.
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mConnectionRequestCallback);
}
@Test
public void testRequestConnectionWithRemoteException() throws RemoteException {
// The first call fails due to a RemoteException.
doThrow(new RemoteException())
.when(mService).requestConnection(anyString(), any(OccupantZoneInfo.class), any());
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mConnectionRequestCallback);
// The second call succeeds. It should not trigger IllegalStateException because
// mConnectionRequestCallback was not added previously.
doNothing()
.when(mService).requestConnection(anyString(), any(OccupantZoneInfo.class), any());
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mConnectionRequestCallback);
}
@Test
public void testRequestConnectionCallbackInvoked() throws RemoteException {
IConnectionRequestCallback[] binderCallback = new IConnectionRequestCallback[1];
doAnswer((invocation) -> {
Object[] args = invocation.getArguments();
String packageName = (String) args[0];
assertThat(packageName).isEqualTo(PACKAGE_NAME);
OccupantZoneInfo receiverZone = (OccupantZoneInfo) args[1];
assertThat(receiverZone).isEqualTo(mReceiverZone);
binderCallback[0] = (IConnectionRequestCallback) args[2];
// Don't call binderCallback[0].onConnected() here, because mConnectionRequestCallback
// is added AFTER this method returns. So if we call binderCallback.onConnected() here,
// mConnectionRequestCallback will not be invoked.
return null;
}).when(mService).requestConnection(anyString(), any(OccupantZoneInfo.class), any());
mOccupantConnectionManager.requestConnection(mReceiverZone, command -> {
command.run();
// Verify that mConnectionRequestCallback is invoked on the Executor.
verify(mConnectionRequestCallback, timeout(1000)).onConnected(mReceiverZone);
}, mConnectionRequestCallback);
binderCallback[0].onConnected(mReceiverZone);
}
@Test
public void testCancelConnectionWithNullParameter_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.cancelConnection(/* receiverZone= */ null));
}
@Test
public void testCancelConnectionWithoutConnectionRequest_throwsException() {
assertThrows(IllegalStateException.class,
() -> mOccupantConnectionManager.cancelConnection(mReceiverZone));
}
@Test
public void testCancelConnection() throws RemoteException {
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mConnectionRequestCallback);
mOccupantConnectionManager.cancelConnection(mReceiverZone);
mOccupantConnectionManager.requestConnection(mReceiverZone, mCallbackExecutor,
mConnectionRequestCallback);
mOccupantConnectionManager.cancelConnection(mReceiverZone);
verify(mService, times(2)).cancelConnection(PACKAGE_NAME, mReceiverZone);
}
@Test
public void testRegisterReceiverWithNullParameters_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.registerReceiver(
/* receiverEndpointId= */ null, mCallbackExecutor, mPayloadCallback));
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.registerReceiver(
RECEIVER_ENDPOINT_ID, /* executor= */ null, mPayloadCallback));
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.registerReceiver(
RECEIVER_ENDPOINT_ID, mCallbackExecutor, /* callback= */ null));
}
@Test
public void testRegisterReceiverWithDuplicateReceiverId_throwsException()
throws RemoteException {
// The first registerReceiver() call should run normally, while the second
// registerReceiver() call should throw an exception because the same ID has been
// registered.
doNothing().doThrow(IllegalStateException.class)
.when(mService).registerReceiver(eq(PACKAGE_NAME), eq(RECEIVER_ENDPOINT_ID), any());
mOccupantConnectionManager.registerReceiver(RECEIVER_ENDPOINT_ID, mCallbackExecutor,
mPayloadCallback);
assertThrows(IllegalStateException.class,
() -> mOccupantConnectionManager.registerReceiver(
RECEIVER_ENDPOINT_ID, mCallbackExecutor, mPayloadCallback));
}
@Test
public void testRegisterReceiverWithRemoteException() throws RemoteException {
// The first call fails due to a RemoteException.
doThrow(new RemoteException())
.when(mService).registerReceiver(anyString(), anyString(), any());
mOccupantConnectionManager.registerReceiver(RECEIVER_ENDPOINT_ID, mCallbackExecutor,
mPayloadCallback);
// The second call succeeds. It should not trigger IllegalStateException because
// mPayloadCallback was not added previously.
doNothing().when(mService).registerReceiver(anyString(), anyString(), any());
mOccupantConnectionManager.registerReceiver(RECEIVER_ENDPOINT_ID, mCallbackExecutor,
mPayloadCallback);
}
@Test
public void testRegisterReceiverCallbackInvoked() throws RemoteException {
OccupantZoneInfo senderZone = mock(OccupantZoneInfo.class);
Payload payload = mock(Payload.class);
IPayloadCallback[] binderCallback = new IPayloadCallback[1];
doAnswer((invocation) -> {
Object[] args = invocation.getArguments();
String packageName = (String) args[0];
assertThat(packageName).isEqualTo(PACKAGE_NAME);
String receiverEndpointId = (String) args[1];
assertThat(receiverEndpointId).isEqualTo(RECEIVER_ENDPOINT_ID);
binderCallback[0] = (IPayloadCallback) args[2];
// Don't call binderCallback[0].onPayloadReceived() here, because mPayloadCallback
// is added AFTER this method returns. So if we call binderCallback.onPayloadReceived()
// here, mPayloadCallback will not be invoked.
return null;
}).when(mService).registerReceiver(anyString(), anyString(), any());
mOccupantConnectionManager.registerReceiver(RECEIVER_ENDPOINT_ID, command -> {
command.run();
// Verify that mPayloadCallback is invoked on the Executor.
verify(mPayloadCallback, timeout(TIMEOUT_MS)).onPayloadReceived(senderZone, payload);
}, mPayloadCallback);
binderCallback[0].onPayloadReceived(senderZone, RECEIVER_ENDPOINT_ID, payload);
}
@Test
public void testUnregisterReceiverWithNullParameter_throwsException() throws RemoteException {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.unregisterReceiver(
/* receiverEndpointId= */ null));
}
@Test
public void testUnregisterReceiver() throws RemoteException {
mOccupantConnectionManager.unregisterReceiver(RECEIVER_ENDPOINT_ID);
verify(mService).unregisterReceiver(PACKAGE_NAME, RECEIVER_ENDPOINT_ID);
}
@Test
public void testSendPayloadNullParameters_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager
.sendPayload(/* receiverZone= */ null, mock(Payload.class)));
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.sendPayload(
mReceiverZone, /* payload= */ null));
}
@Test
public void testSendPayloadWithoutConnection_throwsException() throws RemoteException {
doThrow(IllegalStateException.class)
.when(mService).sendPayload(eq(PACKAGE_NAME), any(), any());
assertThrows(PayloadTransferException.class,
() -> mOccupantConnectionManager.sendPayload(mReceiverZone, mock(Payload.class)));
}
@Test
public void testSendPayload() throws PayloadTransferException, RemoteException {
Payload payload = mock(Payload.class);
mOccupantConnectionManager.sendPayload(mReceiverZone, payload);
verify(mService).sendPayload(PACKAGE_NAME, mReceiverZone, payload);
}
@Test
public void testIsConnectedWithNullParameters_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.isConnected(/* receiverZone= */ null));
}
@Test
public void testIsConnected() throws RemoteException {
when(mService.isConnected(PACKAGE_NAME, mReceiverZone)).thenReturn(true);
boolean isConnected = mOccupantConnectionManager.isConnected(mReceiverZone);
verify(mService).isConnected(PACKAGE_NAME, mReceiverZone);
assertThat(isConnected).isTrue();
}
@Test
public void testDisconnectWithNullParameters_throwsException() {
assertThrows(NullPointerException.class,
() -> mOccupantConnectionManager.disconnect(/* receiverZone= */ null));
}
@Test
public void testDisconnect() throws RemoteException {
mOccupantConnectionManager.disconnect(mReceiverZone);
verify(mService).disconnect(PACKAGE_NAME, mReceiverZone);
}
}