1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.car.cts; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import static org.junit.Assume.assumeNotNull; 22 23 import android.car.Car; 24 import android.car.CarOccupantZoneManager; 25 import android.car.CarOccupantZoneManager.OccupantZoneInfo; 26 import android.car.occupantconnection.AbstractReceiverService; 27 import android.car.occupantconnection.CarOccupantConnectionManager; 28 import android.car.occupantconnection.CarOccupantConnectionManager.ConnectionRequestCallback; 29 import android.car.occupantconnection.Payload; 30 import android.car.test.PermissionsCheckerRule.EnsureHasPermission; 31 import android.car.test.mocks.JavaMockitoHelper; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.ServiceConnection; 36 import android.os.Binder; 37 import android.os.IBinder; 38 import android.platform.test.annotations.AppModeFull; 39 import android.util.Log; 40 import android.util.Pair; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.test.runner.AndroidJUnit4; 45 46 import com.android.compatibility.common.util.ApiTest; 47 import com.android.compatibility.common.util.PollingCheck; 48 49 import org.junit.Before; 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 53 import java.util.ArrayList; 54 import java.util.HexFormat; 55 import java.util.List; 56 import java.util.concurrent.CountDownLatch; 57 import java.util.concurrent.Executor; 58 59 @RunWith(AndroidJUnit4.class) 60 @AppModeFull(reason = "Test relies on other server to connect to.") 61 @EnsureHasPermission({Car.PERMISSION_MANAGE_REMOTE_DEVICE, 62 Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION}) 63 public final class CarOccupantConnectionManagerTest extends AbstractCarTestCase { 64 65 private static final String TAG = CarOccupantConnectionManagerTest.class.getSimpleName(); 66 private static final String RECEIVER_ID = "test_receiver_endpoint_id"; 67 68 private static final long BINDING_TIMEOUT_MS = 3_000; 69 private static final long WAIT_BEFORE_RESPOND_TO_REQUEST_MS = 2_000; 70 private static final long CALLBACK_TIMEOUT_MS = WAIT_BEFORE_RESPOND_TO_REQUEST_MS + 2_000; 71 private static final long EXCHANGE_PAYLOAD_TIMEOUT_MS = 10_000; 72 private static final long POLLING_CHECK_TIMEOUT_MS = 3_000; 73 74 private static final Payload PAYLOAD1 = new Payload(HexFormat.of().parseHex("1234")); 75 private static final Payload PAYLOAD2 = new Payload(HexFormat.of().parseHex("5678")); 76 77 private final Executor mExecutor = mContext.getMainExecutor(); 78 private final TestServiceConnection mServiceConnection = new TestServiceConnection(); 79 80 private CarOccupantConnectionManager mOccupantConnectionManager; 81 82 private TestReceiverService.LocalBinder mBinder; 83 private OccupantZoneInfo mActivePeerZone; 84 85 @Before setUp()86 public void setUp() { 87 mOccupantConnectionManager = getCar().getCarManager(CarOccupantConnectionManager.class); 88 // CarOccupantConnectionManager is available on multi-display builds only. 89 // TODO(b/265091454): annotate the test with @RequireMultipleUsersOnMultipleDisplays. 90 assumeNotNull("Skip the test because CarOccupantConnectionManager is not available on" 91 + " this build", mOccupantConnectionManager); 92 93 CarOccupantZoneManager occupantZoneManager = 94 getCar().getCarManager(CarOccupantZoneManager.class); 95 // Make the sender app request a connection and send Payloads to itself. See b/302567579. 96 mActivePeerZone = occupantZoneManager.getMyOccupantZone(); 97 } 98 99 @Test 100 @ApiTest(apis = { 101 "android.car.occupantconnection.CarOccupantConnectionManager#registerReceiver", 102 "android.car.occupantconnection.CarOccupantConnectionManager#unregisterReceiver", 103 "android.car.occupantconnection.AbstractReceiverService#getAllReceiverEndpoints", 104 "android.car.occupantconnection.AbstractReceiverService#onLocalServiceBind"}) testRegisterAndUnregisterReceiver()105 public void testRegisterAndUnregisterReceiver() throws Exception { 106 mOccupantConnectionManager.registerReceiver(RECEIVER_ID, mExecutor, 107 (senderZone, payload) -> { 108 }); 109 TestReceiverService receiverService = bindToLocalReceiverServiceAndWait(); 110 111 PollingCheck.check("Failed to register the receiver", POLLING_CHECK_TIMEOUT_MS, 112 () -> receiverService.getAllReceiverEndpoints().contains(RECEIVER_ID)); 113 114 mOccupantConnectionManager.unregisterReceiver(RECEIVER_ID); 115 116 PollingCheck.check("Failed to unregister the receiver", POLLING_CHECK_TIMEOUT_MS, 117 () -> receiverService.getAllReceiverEndpoints().isEmpty()); 118 } 119 120 @Test 121 @ApiTest(apis = { 122 "android.car.occupantconnection.CarOccupantConnectionManager#requestConnection", 123 "android.car.occupantconnection.CarOccupantConnectionManager#cancelConnection"}) testRequestAndCancelConnection()124 public void testRequestAndCancelConnection() { 125 ConnectionRequestCallback connectionRequestCallback = new ConnectionRequestCallback() { 126 @Override 127 public void onConnected(@NonNull OccupantZoneInfo receiverZone) { 128 } 129 130 @Override 131 public void onFailed(@NonNull OccupantZoneInfo receiverZone, 132 int connectionError) { 133 } 134 135 @Override 136 public void onDisconnected(@NonNull OccupantZoneInfo receiverZone) { 137 } 138 }; 139 140 mOccupantConnectionManager.requestConnection(mActivePeerZone, mExecutor, 141 connectionRequestCallback); 142 // No exception should be thrown. 143 mOccupantConnectionManager.cancelConnection(mActivePeerZone); 144 145 mOccupantConnectionManager.requestConnection(mActivePeerZone, mExecutor, 146 connectionRequestCallback); 147 mOccupantConnectionManager.cancelConnection(mActivePeerZone); 148 } 149 150 /** 151 * Test: 152 * <ul> 153 * <li> The sender requests a connection to the receiver. Then the receiver rejects it. 154 * <li> The sender requests another connection to the receiver. Then the receiver accepts it. 155 * <li> The sender sends PAYLOAD1 to the receiver. Then the receiver verifies PAYLOAD1, 156 * requests two connections to the sender (the first request will be rejected, while 157 * the second one will be accepted), and sends PAYLOAD2 to the sender. Then the sender 158 * verifies PAYLOAD2. 159 * <li> The sender disconnects. 160 * </ul> 161 */ 162 @Test 163 @ApiTest(apis = { 164 "android.car.occupantconnection.CarOccupantConnectionManager#requestConnection", 165 "android.car.occupantconnection.CarOccupantConnectionManager#isConnected", 166 "android.car.occupantconnection.CarOccupantConnectionManager#sendPayload", 167 "android.car.occupantconnection.CarOccupantConnectionManager#disconnect", 168 "android.car.occupantconnection.AbstractReceiverService#acceptConnection", 169 "android.car.occupantconnection.AbstractReceiverService#rejectConnection", 170 "android.car.occupantconnection.AbstractReceiverService#onPayloadReceived"}) testConnectAndSendPayload()171 public void testConnectAndSendPayload() 172 throws CarOccupantConnectionManager.PayloadTransferException { 173 TestReceiverService receiverService = bindToLocalReceiverServiceAndWait(); 174 175 boolean[] onConnectedInvoked = new boolean[1]; 176 boolean[] onFailedInvoked = new boolean[1]; 177 int[] connectionErrors = new int[1]; 178 ConnectionRequestCallback connectionRequestCallback = new ConnectionRequestCallback() { 179 @Override 180 public void onConnected(@NonNull OccupantZoneInfo receiverZone) { 181 onConnectedInvoked[0] = true; 182 } 183 184 @Override 185 public void onFailed(@NonNull OccupantZoneInfo receiverZone, 186 int connectionError) { 187 onFailedInvoked[0] = true; 188 connectionErrors[0] = connectionError; 189 } 190 191 @Override 192 public void onDisconnected(@NonNull OccupantZoneInfo receiverZone) { 193 } 194 }; 195 196 // The receiver service will reject the first request. 197 Log.v(TAG, "Sender requests a connection for the first time"); 198 mOccupantConnectionManager.requestConnection(mActivePeerZone, mExecutor, 199 connectionRequestCallback); 200 PollingCheck.waitFor(CALLBACK_TIMEOUT_MS, 201 () -> !onConnectedInvoked[0] && onFailedInvoked[0] 202 && connectionErrors[0] == TestReceiverService.REJECTION_REASON); 203 Log.v(TAG, "Sender's first request is rejected"); 204 205 // The receiver service will accept the second request. 206 Log.v(TAG, "Sender requests another connection"); 207 onConnectedInvoked[0] = false; 208 onFailedInvoked[0] = false; 209 mOccupantConnectionManager.requestConnection(mActivePeerZone, mExecutor, 210 connectionRequestCallback); 211 PollingCheck.waitFor(CALLBACK_TIMEOUT_MS, 212 () -> onConnectedInvoked[0] && !onFailedInvoked[0]); 213 Log.v(TAG, "Sender's second request is accepted"); 214 215 assertWithMessage("It should be connected to %s", mActivePeerZone) 216 .that(mOccupantConnectionManager.isConnected(mActivePeerZone)).isTrue(); 217 218 Log.v(TAG, "Sender sends PAYLOAD1 to the receiver"); 219 mOccupantConnectionManager.sendPayload(mActivePeerZone, PAYLOAD1); 220 Pair<OccupantZoneInfo, Payload> expectedResponse = new Pair<>(mActivePeerZone, PAYLOAD2); 221 PollingCheck.waitFor(EXCHANGE_PAYLOAD_TIMEOUT_MS, 222 () -> receiverService.mOnPayloadReceivedInvokedRecords.contains(expectedResponse)); 223 Log.v(TAG, "Sender receives PAYLOAD2 from the receiver"); 224 225 mOccupantConnectionManager.disconnect(mActivePeerZone); 226 assertWithMessage("Sender should be disconnected to %s", mActivePeerZone) 227 .that(mOccupantConnectionManager.isConnected(mActivePeerZone)).isFalse(); 228 } 229 bindToLocalReceiverServiceAndWait()230 private TestReceiverService bindToLocalReceiverServiceAndWait() { 231 Log.v(TAG, "Binding to local receiver service"); 232 Intent intent = new Intent(); 233 intent.setClassName(mContext, TestReceiverService.class.getName()); 234 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 235 try { 236 JavaMockitoHelper.await(mServiceConnection.latch, BINDING_TIMEOUT_MS); 237 } catch (InterruptedException e) { 238 throw new RuntimeException(e); 239 } 240 Log.v(TAG, "Local receiver service bounded"); 241 return mBinder.getService(); 242 } 243 244 private final class TestServiceConnection implements ServiceConnection { 245 246 public final CountDownLatch latch = new CountDownLatch(1); 247 248 @Override onServiceConnected(ComponentName name, IBinder service)249 public void onServiceConnected(ComponentName name, IBinder service) { 250 mBinder = (TestReceiverService.LocalBinder) service; 251 latch.countDown(); 252 } 253 254 @Override onServiceDisconnected(ComponentName name)255 public void onServiceDisconnected(ComponentName name) { 256 } 257 } 258 259 /** 260 * An implementation of AbstractReceiverService. 261 * <p> 262 * This service will wait for a while before responding to a connection request (to allow the 263 * sender to cancel the request). After that, it will reject the first request, and accept the 264 * second request. 265 * <p> 266 * When this service receives PAYLOAD1 from the sender, it will send two connection requests to 267 * the sender's receiver service. The first request wil be rejected, while the second request 268 * will be accepted. Once it is accepted, it will send PAYLOAD2 to the sender. 269 */ 270 public static class TestReceiverService extends AbstractReceiverService { 271 272 private static final int REJECTION_REASON = 123; 273 274 // The following lists are used to verify an onFoo() method was invoked with certain 275 // parameters. 276 private final List<Pair<OccupantZoneInfo, Payload>> mOnPayloadReceivedInvokedRecords = 277 new ArrayList<>(); 278 279 private final LocalBinder mLocalBinder = new LocalBinder(); 280 281 private Car mCar; 282 private CarOccupantConnectionManager mOccupantConnectionManager; 283 private Object mOnConnectionInitiatedLock = new Object(); 284 private boolean mRejected = false; 285 286 private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener = 287 (car, ready) -> { 288 if (!ready) { 289 mOccupantConnectionManager = null; 290 return; 291 } 292 mCar = car; 293 mOccupantConnectionManager = car.getCarManager( 294 CarOccupantConnectionManager.class); 295 }; 296 297 private class LocalBinder extends Binder { 298 getService()299 TestReceiverService getService() { 300 return TestReceiverService.this; 301 } 302 } 303 304 @Override onPayloadReceived(OccupantZoneInfo senderZone, Payload payload)305 public void onPayloadReceived(OccupantZoneInfo senderZone, Payload payload) { 306 mOnPayloadReceivedInvokedRecords.add(new Pair(senderZone, payload)); 307 PollingCheck.waitFor(CALLBACK_TIMEOUT_MS, () -> mOccupantConnectionManager != null); 308 309 if (PAYLOAD1.equals(payload)) { 310 try { 311 Log.v(TAG, "The receiver service just sends PAYLOAD2 to the sender" 312 + " without establishing a new connection since they run in the" 313 + " same occupant zone"); 314 mOccupantConnectionManager.sendPayload(senderZone, PAYLOAD2); 315 } catch (CarOccupantConnectionManager.PayloadTransferException e) { 316 throw new RuntimeException(e); 317 } 318 } 319 } 320 321 @Override onReceiverRegistered(String receiverEndpointId)322 public void onReceiverRegistered(String receiverEndpointId) { 323 Log.v(TAG, "onReceiverRegistered:" + receiverEndpointId); 324 } 325 326 @Override onConnectionInitiated(OccupantZoneInfo senderZone)327 public void onConnectionInitiated(OccupantZoneInfo senderZone) { 328 // Wait a while to allow some time for the sender to cancel the request. 329 try { 330 Thread.sleep(WAIT_BEFORE_RESPOND_TO_REQUEST_MS); 331 } catch (InterruptedException e) { 332 throw new RuntimeException(e); 333 } 334 335 synchronized (mOnConnectionInitiatedLock) { 336 // If the sender didn't cancel the request, reject the first request, and accept 337 // the second request. 338 if (!mRejected) { 339 rejectConnection(senderZone, REJECTION_REASON); 340 mRejected = true; 341 } else { 342 acceptConnection(senderZone); 343 } 344 } 345 } 346 347 @Override onConnected(OccupantZoneInfo senderZone)348 public void onConnected(OccupantZoneInfo senderZone) { 349 } 350 351 @Override onConnectionCanceled(OccupantZoneInfo senderZone)352 public void onConnectionCanceled(OccupantZoneInfo senderZone) { 353 } 354 355 @Override onDisconnected(OccupantZoneInfo senderZone)356 public void onDisconnected(OccupantZoneInfo senderZone) { 357 } 358 359 @Override onCreate()360 public void onCreate() { 361 super.onCreate(); 362 mCar = Car.createCar(this, /* handler= */ null, 363 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener); 364 } 365 366 @Override onDestroy()367 public void onDestroy() { 368 if (mCar != null && mCar.isConnected()) { 369 mCar.disconnect(); 370 mCar = null; 371 } 372 super.onDestroy(); 373 } 374 375 @Nullable 376 @Override onLocalServiceBind(@onNull Intent intent)377 public IBinder onLocalServiceBind(@NonNull Intent intent) { 378 return mLocalBinder; 379 } 380 } 381 } 382