Check when device is unavailable that it's gone

Ensure if device is really gone or not before marking it
as FREE_UNKNOWN which could results in removing the device
from the device list.

Test: unit tests
Bug: 62088635
Change-Id: If4fad78cb61b7307bbea19df5a774f6ddbd83d79
(cherry picked from commit 50f55c8ebce12c91c3f290d87e626945823148a1)
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 06ae4c8..b1a0667 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -59,6 +59,7 @@
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 
 @OptionClass(alias = "dmgr", global_namespace = false)
 public class DeviceManager implements IDeviceManager {
@@ -85,6 +86,17 @@
     private static final String EMULATOR_SERIAL_PREFIX = "emulator";
     private static final String TCP_DEVICE_SERIAL_PREFIX = "tcp-device";
 
+    /**
+     * Pattern for a device listed by 'adb devices':
+     *
+     * <p>List of devices attached
+     *
+     * <p>serial1 device
+     *
+     * <p>serial2 device
+     */
+    private static final String DEVICE_LIST_PATTERN = "(.*)(\n)(%s)(\\s+)(device)(.*?)";
+
     protected DeviceMonitorMultiplexer mDvcMon = new DeviceMonitorMultiplexer();
 
     private boolean mIsInitialized = false;
@@ -528,17 +540,28 @@
     /**
      * Helper method to convert from a {@link com.android.tradefed.device.FreeDeviceState} to a
      * {@link com.android.tradefed.device.DeviceEvent}
+     *
      * @param managedDevice
      */
-    static DeviceEvent getEventFromFree(IManagedTestDevice managedDevice, FreeDeviceState deviceState) {
+    private DeviceEvent getEventFromFree(
+            IManagedTestDevice managedDevice, FreeDeviceState deviceState) {
         switch (deviceState) {
             case UNRESPONSIVE:
                 return DeviceEvent.FREE_UNRESPONSIVE;
             case AVAILABLE:
                 return DeviceEvent.FREE_AVAILABLE;
             case UNAVAILABLE:
-                if (managedDevice.getDeviceState() == TestDeviceState.NOT_AVAILABLE) {
-                    return DeviceEvent.FREE_UNKNOWN;
+                // We double check if device is still showing in adb or not to confirm the
+                // connection is gone.
+                if (TestDeviceState.NOT_AVAILABLE.equals(managedDevice.getDeviceState())) {
+                    String devices = executeGlobalAdbCommand("devices");
+                    Pattern p =
+                            Pattern.compile(
+                                    String.format(
+                                            DEVICE_LIST_PATTERN, managedDevice.getSerialNumber()));
+                    if (devices == null || !p.matcher(devices).find()) {
+                        return DeviceEvent.FREE_UNKNOWN;
+                    }
                 }
                 return DeviceEvent.FREE_UNAVAILABLE;
             case IGNORE:
diff --git a/tests/src/com/android/tradefed/device/DeviceManagerTest.java b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
index 16dfbb9..ee0f797 100644
--- a/tests/src/com/android/tradefed/device/DeviceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
@@ -115,7 +115,6 @@
         public int waitFor() throws InterruptedException {
             return 0;
         }
-
     }
 
     /**
@@ -890,6 +889,197 @@
     }
 
     /**
+     * Test freeing a device that was unable but showing in adb devices. Device will become
+     * Unavailable but still seen by the DeviceManager.
+     */
+    public void testFreeDevice_unavailable() {
+        EasyMock.expect(mMockIDevice.isEmulator()).andStubReturn(Boolean.FALSE);
+        EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceShell(EasyMock.anyLong()))
+                .andReturn(Boolean.TRUE);
+        mMockStateMonitor.setState(TestDeviceState.NOT_AVAILABLE);
+
+        CommandResult stubAdbDevices = new CommandResult(CommandStatus.SUCCESS);
+        stubAdbDevices.setStdout("List of devices attached\nserial\tdevice\n");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(), EasyMock.eq("adb"), EasyMock.eq("devices")))
+                .andReturn(stubAdbDevices);
+
+        replayMocks();
+        IManagedTestDevice testDevice = new TestDevice(mMockIDevice, mMockStateMonitor, null);
+        DeviceManager manager = createDeviceManagerNoInit();
+        manager.init(
+                null,
+                null,
+                new ManagedTestDeviceFactory(false, null, null) {
+                    @Override
+                    public IManagedTestDevice createDevice(IDevice idevice) {
+                        mMockTestDevice.setIDevice(idevice);
+                        return testDevice;
+                    }
+
+                    @Override
+                    protected CollectingOutputReceiver createOutputReceiver() {
+                        return new CollectingOutputReceiver() {
+                            @Override
+                            public String getOutput() {
+                                return "/system/bin/pm";
+                            }
+                        };
+                    }
+
+                    @Override
+                    public void setFastbootEnabled(boolean enable) {
+                        // ignore
+                    }
+                });
+
+        mDeviceListener.deviceConnected(mMockIDevice);
+
+        IManagedTestDevice device = (IManagedTestDevice) manager.allocateDevice();
+        assertNotNull(device);
+        // device becomes unavailable
+        device.setDeviceState(TestDeviceState.NOT_AVAILABLE);
+        // a freed 'unavailable' device becomes UNAVAILABLE state
+        manager.freeDevice(device, FreeDeviceState.UNAVAILABLE);
+        // ensure device cannot be allocated again
+        ITestDevice device2 = manager.allocateDevice();
+        assertNull(device2);
+        verifyMocks();
+        // We still have the device in the list
+        assertEquals(1, manager.getDeviceList().size());
+    }
+
+    /**
+     * Test that when freeing an Unavailable device that is not in 'adb devices' we correctly remove
+     * it from our tracking list.
+     */
+    public void testFreeDevice_unknown() {
+        EasyMock.expect(mMockIDevice.isEmulator()).andStubReturn(Boolean.FALSE);
+        EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceShell(EasyMock.anyLong()))
+                .andReturn(Boolean.TRUE);
+        mMockStateMonitor.setState(TestDeviceState.NOT_AVAILABLE);
+
+        CommandResult stubAdbDevices = new CommandResult(CommandStatus.SUCCESS);
+        // device serial is not in the list
+        stubAdbDevices.setStdout("List of devices attached\n");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(), EasyMock.eq("adb"), EasyMock.eq("devices")))
+                .andReturn(stubAdbDevices);
+
+        replayMocks();
+        IManagedTestDevice testDevice = new TestDevice(mMockIDevice, mMockStateMonitor, null);
+        DeviceManager manager = createDeviceManagerNoInit();
+        manager.init(
+                null,
+                null,
+                new ManagedTestDeviceFactory(false, null, null) {
+                    @Override
+                    public IManagedTestDevice createDevice(IDevice idevice) {
+                        mMockTestDevice.setIDevice(idevice);
+                        return testDevice;
+                    }
+
+                    @Override
+                    protected CollectingOutputReceiver createOutputReceiver() {
+                        return new CollectingOutputReceiver() {
+                            @Override
+                            public String getOutput() {
+                                return "/system/bin/pm";
+                            }
+                        };
+                    }
+
+                    @Override
+                    public void setFastbootEnabled(boolean enable) {
+                        // ignore
+                    }
+                });
+
+        mDeviceListener.deviceConnected(mMockIDevice);
+
+        IManagedTestDevice device = (IManagedTestDevice) manager.allocateDevice();
+        assertNotNull(device);
+        // device becomes unavailable
+        device.setDeviceState(TestDeviceState.NOT_AVAILABLE);
+        // a freed 'unavailable' device becomes UNAVAILABLE state
+        manager.freeDevice(device, FreeDeviceState.UNAVAILABLE);
+        // ensure device cannot be allocated again
+        ITestDevice device2 = manager.allocateDevice();
+        assertNull(device2);
+        verifyMocks();
+        // We have 0 device in the list since it was removed
+        assertEquals(0, manager.getDeviceList().size());
+    }
+
+    /**
+     * Test that when freeing an Unavailable device that is not in 'adb devices' we correctly remove
+     * it from our tracking list even if its serial is a substring of another serial.
+     */
+    public void testFreeDevice_unknown_subName() {
+        EasyMock.expect(mMockIDevice.isEmulator()).andStubReturn(Boolean.FALSE);
+        EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceShell(EasyMock.anyLong()))
+                .andReturn(Boolean.TRUE);
+        mMockStateMonitor.setState(TestDeviceState.NOT_AVAILABLE);
+
+        CommandResult stubAdbDevices = new CommandResult(CommandStatus.SUCCESS);
+        // device serial is not in the list
+        stubAdbDevices.setStdout("List of devices attached\n2serial\tdevice\n");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(), EasyMock.eq("adb"), EasyMock.eq("devices")))
+                .andReturn(stubAdbDevices);
+
+        replayMocks();
+        IManagedTestDevice testDevice = new TestDevice(mMockIDevice, mMockStateMonitor, null);
+        DeviceManager manager = createDeviceManagerNoInit();
+        manager.init(
+                null,
+                null,
+                new ManagedTestDeviceFactory(false, null, null) {
+                    @Override
+                    public IManagedTestDevice createDevice(IDevice idevice) {
+                        mMockTestDevice.setIDevice(idevice);
+                        return testDevice;
+                    }
+
+                    @Override
+                    protected CollectingOutputReceiver createOutputReceiver() {
+                        return new CollectingOutputReceiver() {
+                            @Override
+                            public String getOutput() {
+                                return "/system/bin/pm";
+                            }
+                        };
+                    }
+
+                    @Override
+                    public void setFastbootEnabled(boolean enable) {
+                        // ignore
+                    }
+                });
+
+        mDeviceListener.deviceConnected(mMockIDevice);
+
+        IManagedTestDevice device = (IManagedTestDevice) manager.allocateDevice();
+        assertNotNull(device);
+        // device becomes unavailable
+        device.setDeviceState(TestDeviceState.NOT_AVAILABLE);
+        // a freed 'unavailable' device becomes UNAVAILABLE state
+        manager.freeDevice(device, FreeDeviceState.UNAVAILABLE);
+        // ensure device cannot be allocated again
+        ITestDevice device2 = manager.allocateDevice();
+        assertNull(device2);
+        verifyMocks();
+        // We have 0 device in the list since it was removed
+        assertEquals(0, manager.getDeviceList().size());
+    }
+
+    /**
      * Helper to set the expectation when a {@link DeviceDescriptor} is expected.
      */
     private void setDeviceDescriptorExpectation() {