Stop advertising for reconect after timeout

Fixes: 151252960
Test: Unit tests and manually verify reconnect times out
Change-Id: Ie1de76c871b7d0bcfc4d05b6f5993e9573d57935
diff --git a/connected-device-lib/res/values/config.xml b/connected-device-lib/res/values/config.xml
index c052264..0f88ddc 100644
--- a/connected-device-lib/res/values/config.xml
+++ b/connected-device-lib/res/values/config.xml
@@ -25,4 +25,6 @@
     <string name="car_secure_write_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
 
     <string name="connected_device_shared_preferences" translatable="false">com.android.car.connecteddevice</string>
+
+    <integer name="car_reconnect_timeout_sec">60</integer>
 </resources>
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
index 8218a94..f10bb85 100644
--- a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
@@ -107,6 +107,8 @@
 
     private final AtomicBoolean mIsConnectingToUserDevice = new AtomicBoolean(false);
 
+    private final int mReconnectTimeoutSeconds;
+
     private String mNameForAssociation;
 
     private AssociationCallback mAssociationCallback;
@@ -147,7 +149,8 @@
                 UUID.fromString(context.getString(R.string.car_association_service_uuid)),
                 context.getString(R.string.car_bg_mask),
                 UUID.fromString(context.getString(R.string.car_secure_write_uuid)),
-                UUID.fromString(context.getString(R.string.car_secure_read_uuid)));
+                UUID.fromString(context.getString(R.string.car_secure_read_uuid)),
+                context.getResources().getInteger(R.integer.car_reconnect_timeout_sec));
     }
 
     private ConnectedDeviceManager(
@@ -159,19 +162,21 @@
             @NonNull UUID associationServiceUuid,
             @NonNull String bgMask,
             @NonNull UUID writeCharacteristicUuid,
-            @NonNull UUID readCharacteristicUuid) {
+            @NonNull UUID readCharacteristicUuid,
+            int reconnectTimeoutSeconds) {
         this(storage,
                 new CarBleCentralManager(context, bleCentralManager, storage, serviceUuid, bgMask,
                         writeCharacteristicUuid, readCharacteristicUuid),
                 new CarBlePeripheralManager(blePeripheralManager, storage, associationServiceUuid,
-                        writeCharacteristicUuid, readCharacteristicUuid));
+                        writeCharacteristicUuid, readCharacteristicUuid), reconnectTimeoutSeconds);
     }
 
     @VisibleForTesting
     ConnectedDeviceManager(
             @NonNull ConnectedDeviceStorage storage,
             @NonNull CarBleCentralManager centralManager,
-            @NonNull CarBlePeripheralManager peripheralManager) {
+            @NonNull CarBlePeripheralManager peripheralManager,
+            int reconnectTimeoutSeconds) {
         Executor callbackExecutor = Executors.newSingleThreadExecutor();
         mStorage = storage;
         mCentralManager = centralManager;
@@ -180,6 +185,7 @@
         mPeripheralManager.registerCallback(generateCarBleCallback(peripheralManager),
                 callbackExecutor);
         mStorage.setAssociatedDeviceCallback(mAssociatedDeviceCallback);
+        mReconnectTimeoutSeconds = reconnectTimeoutSeconds;
     }
 
     /**
@@ -296,7 +302,8 @@
             }
             EventLog.onStartDeviceSearchStarted();
             mIsConnectingToUserDevice.set(true);
-            mPeripheralManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
+            mPeripheralManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()),
+                    mReconnectTimeoutSeconds);
         } catch (Exception e) {
             loge(TAG, "Exception while attempting connection with active user's device.", e);
         }
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
index 5dec98d..e3584d6 100644
--- a/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
@@ -31,6 +31,8 @@
 import android.bluetooth.le.AdvertiseData;
 import android.bluetooth.le.AdvertiseSettings;
 import android.car.encryptionrunner.EncryptionRunnerFactory;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.ParcelUuid;
 
 import com.android.car.connecteddevice.AssociationCallback;
@@ -79,6 +81,8 @@
 
     private final BluetoothGattCharacteristic mReadCharacteristic;
 
+    private final Handler mTimeoutHandler;
+
     // BLE default is 23, minus 3 bytes for ATT_PROTOCOL.
     private int mWriteSize = 20;
 
@@ -117,6 +121,7 @@
                         | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
         mReadCharacteristic.addDescriptor(mDescriptor);
+        mTimeoutHandler = new Handler(Looper.getMainLooper());
     }
 
     @Override
@@ -143,8 +148,8 @@
         mConnectedDevices.clear();
     }
 
-    /** Connect to device with provided id. */
-    public void connectToDevice(@NonNull UUID deviceId) {
+    /** Attempt to connect to device with provided id within set timeout period. */
+    public void connectToDevice(@NonNull UUID deviceId, int timeoutSeconds) {
         for (BleDevice device : mConnectedDevices) {
             if (UUID.fromString(device.mDeviceId).equals(deviceId)) {
                 logd(TAG, "Already connected to device " + deviceId + ".");
@@ -160,11 +165,15 @@
             @Override
             public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                 super.onStartSuccess(settingsInEffect);
-                logd(TAG, "Successfully started advertising for device " + deviceId + ".");
+                mTimeoutHandler.postDelayed(mTimeoutRunnable,
+                        TimeUnit.SECONDS.toMillis(timeoutSeconds));
+                logd(TAG, "Successfully started advertising for device " + deviceId
+                        + " for " + timeoutSeconds + " seconds.");
             }
         };
         mBlePeripheralManager.unregisterCallback(mAssociationPeripheralCallback);
         mBlePeripheralManager.registerCallback(mReconnectPeripheralCallback);
+        mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
         startAdvertising(deviceId, mAdvertiseCallback, /* includeDeviceName = */ false);
     }
 
@@ -314,6 +323,7 @@
     private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
         EventLog.onDeviceConnected();
         mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
+        mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
         mClientDeviceAddress = device.getAddress();
         mClientDeviceName = device.getName();
         if (mClientDeviceName == null) {
@@ -512,4 +522,12 @@
                     setDeviceId(deviceId);
                 }
             };
+
+    private final Runnable mTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            logd(TAG, "Timeout period expired without a connection. Stopping advertisement.");
+            mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
+        }
+    };
 }
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
index 3d48578..579fe7d 100644
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
@@ -22,6 +22,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mockitoSession;
 import static org.mockito.Mockito.spy;
@@ -72,6 +73,8 @@
 
     private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
 
+    private static final int DEFAULT_RECONNECT_TIMEOUT = 5;
+
     private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
 
     private final UUID mRecipientId = UUID.randomUUID();
@@ -100,7 +103,7 @@
         ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor = ArgumentCaptor
                 .forClass(AssociatedDeviceCallback.class);
         mConnectedDeviceManager = new ConnectedDeviceManager(mMockStorage, mMockCentralManager,
-            mMockPeripheralManager);
+            mMockPeripheralManager, DEFAULT_RECONNECT_TIMEOUT);
         verify(mMockStorage).setAssociatedDeviceCallback(callbackCaptor.capture());
         mAssociatedDeviceCallback = callbackCaptor.getValue();
         mConnectedDeviceManager.start();
@@ -552,7 +555,7 @@
         mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
         mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
         verify(mMockPeripheralManager, timeout(1000))
-                .connectToDevice(eq(UUID.fromString(deviceId)));
+                .connectToDevice(eq(UUID.fromString(deviceId)), anyInt());
     }
 
     @Test
@@ -568,7 +571,7 @@
         mConnectedDeviceManager.addConnectedDevice(deviceId, mMockPeripheralManager);
         mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
         verify(mMockPeripheralManager, timeout(1000))
-                .connectToDevice(eq(UUID.fromString(userDeviceId)));
+                .connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
     }
 
     @Test
@@ -585,7 +588,7 @@
         mConnectedDeviceManager.addConnectedDevice(userDeviceId, mMockCentralManager);
         mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
         verify(mMockPeripheralManager, timeout(1000).times(0))
-                .connectToDevice(eq(UUID.fromString(userDeviceId)));
+                .connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
     }
 
     @Test
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
index adba67b..986f11e 100644
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
@@ -92,7 +92,9 @@
 
     @After
     public void tearDown() {
-        mCarBlePeripheralManager.stop();
+        if (mCarBlePeripheralManager != null) {
+            mCarBlePeripheralManager.stop();
+        }
         if (mMockitoSession != null) {
             mMockitoSession.finishMocking();
         }
@@ -195,6 +197,18 @@
         verify(callback).onAssociationError(eq(testErrorCode));
     }
 
+    @Test
+    public void connectToDevice_stopsAdvertisingAfterTimeout() {
+        int timeoutSeconds = 2;
+        mCarBlePeripheralManager.connectToDevice(UUID.randomUUID(), timeoutSeconds);
+        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
+                ArgumentCaptor.forClass(AdvertiseCallback.class);
+        verify(mMockPeripheralManager).startAdvertising(any(), any(), callbackCaptor.capture());
+        callbackCaptor.getValue().onStartSuccess(null);
+        verify(mMockPeripheralManager, timeout(TimeUnit.SECONDS.toMillis(timeoutSeconds + 1)))
+                .stopAdvertising(any(AdvertiseCallback.class));
+    }
+
     private BlePeripheralManager.Callback startAssociation(AssociationCallback callback,
             String deviceName) {
         ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =