diff options
author | Juan C Nuno <juancnuno@google.com> | 2022-07-12 14:51:33 -0700 |
---|---|---|
committer | Juan Nuno <juancnuno@google.com> | 2022-07-14 18:19:55 +0000 |
commit | 48d7630a33065584b5ec408d21865bf7b04fb8cb (patch) | |
tree | 1231ac086156b7e071686cc692aefeac9fd6ae13 | |
parent | fda264027d9d103e36aa5e0784d082bbf682a4cb (diff) | |
download | idea-48d7630a33065584b5ec408d21865bf7b04fb8cb.tar.gz |
Implement stopping in the model
Bug: 237442318
Test: VirtualDeviceTableModelTest
Change-Id: I617c393c98b361c165a7456876e2bc9b8b1a58c7
2 files changed, 226 insertions, 20 deletions
diff --git a/device-manager/src/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModel.java b/device-manager/src/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModel.java index 70f23011b01..ae35e45f5d1 100644 --- a/device-manager/src/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModel.java +++ b/device-manager/src/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModel.java @@ -16,10 +16,14 @@ package com.android.tools.idea.devicemanager.virtualtab; import com.android.annotations.concurrency.UiThread; +import com.android.annotations.concurrency.WorkerThread; +import com.android.ddmlib.EmulatorConsole; +import com.android.ddmlib.IDevice; import com.android.sdklib.AndroidVersion; import com.android.tools.idea.avdmanager.AvdManagerConnection; import com.android.tools.idea.devicemanager.ActivateDeviceFileExplorerWindowValue; import com.android.tools.idea.devicemanager.Device; +import com.android.tools.idea.devicemanager.DeviceManagerAndroidDebugBridge; import com.android.tools.idea.devicemanager.DeviceManagerFutureCallback; import com.android.tools.idea.devicemanager.DeviceManagerUsageTracker; import com.android.tools.idea.devicemanager.Devices; @@ -31,6 +35,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.wireless.android.sdk.stats.DeviceManagerEvent; +import com.google.wireless.android.sdk.stats.DeviceManagerEvent.EventKind; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.util.concurrency.AppExecutorUtil; @@ -40,10 +45,13 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Function; import javax.swing.table.AbstractTableModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +// TODO Annotate the methods with the threading annotations @UiThread final class VirtualDeviceTableModel extends AbstractTableModel { static final int DEVICE_MODEL_COLUMN_INDEX = 0; @@ -56,8 +64,12 @@ final class VirtualDeviceTableModel extends AbstractTableModel { private final @Nullable Project myProject; private @NotNull List<@NotNull VirtualDevice> myDevices; - private final @NotNull Callable<@NotNull AvdManagerConnection> myGetDefaultAvdManagerConnection; private final @NotNull NewSetOnline myNewSetOnline; + private final @NotNull Callable<@NotNull AvdManagerConnection> myGetDefaultAvdManagerConnection; + private final @NotNull Function<@NotNull VirtualDeviceTableModel, @NotNull FutureCallback<@Nullable Object>> myNewSetAllOnline; + private final @NotNull DeviceManagerAndroidDebugBridge myBridge; + private final @NotNull Function<@NotNull IDevice, @NotNull EmulatorConsole> myGetConsole; + private final @NotNull BiConsumer<@NotNull Throwable, @Nullable Project> myShowErrorDialog; @VisibleForTesting interface NewSetOnline { @@ -77,23 +89,33 @@ final class VirtualDeviceTableModel extends AbstractTableModel { } VirtualDeviceTableModel(@Nullable Project project) { - this(project, List.of()); - } - - @VisibleForTesting - VirtualDeviceTableModel(@Nullable Project project, @NotNull Collection<@NotNull VirtualDevice> devices) { - this(project, devices, AvdManagerConnection::getDefaultAvdManagerConnection, VirtualDeviceTableModel::newSetOnline); + this(project, + List.of(), + VirtualDeviceTableModel::newSetOnline, + AvdManagerConnection::getDefaultAvdManagerConnection, + SetAllOnline::new, + new DeviceManagerAndroidDebugBridge(), + EmulatorConsole::getConsole, + VirtualTabMessages::showErrorDialog); } @VisibleForTesting VirtualDeviceTableModel(@Nullable Project project, @NotNull Collection<@NotNull VirtualDevice> devices, + @NotNull NewSetOnline newSetOnline, @NotNull Callable<@NotNull AvdManagerConnection> getDefaultAvdManagerConnection, - @NotNull NewSetOnline newSetOnline) { + @NotNull Function<@NotNull VirtualDeviceTableModel, @NotNull FutureCallback<@Nullable Object>> newSetAllOnline, + @NotNull DeviceManagerAndroidDebugBridge bridge, + @NotNull Function<@NotNull IDevice, @NotNull EmulatorConsole> getConsole, + @NotNull BiConsumer<@NotNull Throwable, @Nullable Project> showErrorDialog) { myProject = project; myDevices = new ArrayList<>(devices); - myGetDefaultAvdManagerConnection = getDefaultAvdManagerConnection; myNewSetOnline = newSetOnline; + myGetDefaultAvdManagerConnection = getDefaultAvdManagerConnection; + myNewSetAllOnline = newSetAllOnline; + myBridge = bridge; + myGetConsole = getConsole; + myShowErrorDialog = showErrorDialog; } @VisibleForTesting @@ -307,7 +329,7 @@ final class VirtualDeviceTableModel extends AbstractTableModel { assert false; break; case STOPPING: - // TODO stop + stop(modelRowIndex); break; default: assert false : state; @@ -317,7 +339,7 @@ final class VirtualDeviceTableModel extends AbstractTableModel { private void launch(int modelRowIndex) { DeviceManagerEvent event = DeviceManagerEvent.newBuilder() - .setKind(DeviceManagerEvent.EventKind.VIRTUAL_LAUNCH_ACTION) + .setKind(EventKind.VIRTUAL_LAUNCH_ACTION) .build(); DeviceManagerUsageTracker.log(event); @@ -332,18 +354,54 @@ final class VirtualDeviceTableModel extends AbstractTableModel { // noinspection UnstableApiUsage FluentFuture.from(getDefaultAvdManagerConnection()) .transformAsync(connection -> connection.startAvd(myProject, device.getAvdInfo()), executor) - .addCallback(new SetAllOnline(this), executor); + .addCallback(myNewSetAllOnline.apply(this), executor); } - private static final class SetAllOnline implements FutureCallback<Object> { + private void stop(int modelRowIndex) { + DeviceManagerEvent event = DeviceManagerEvent.newBuilder() + .setKind(EventKind.VIRTUAL_STOP_ACTION) + .build(); + + DeviceManagerUsageTracker.log(event); + + VirtualDevice device = myDevices.get(modelRowIndex).withState(VirtualDevice.State.STOPPING); + + myDevices.set(modelRowIndex, device); + fireTableCellUpdated(modelRowIndex, LAUNCH_OR_STOP_MODEL_COLUMN_INDEX); + + // noinspection UnstableApiUsage + FluentFuture.from(myBridge.findDevice(myProject, device.getKey())) + .transform(d -> stop(d, device), AppExecutorUtil.getAppExecutorService()) + .addCallback(myNewSetAllOnline.apply(this), EdtExecutorService.getInstance()); + } + + /** + * Called by an application pool thread + */ + @WorkerThread + @SuppressWarnings("SameReturnValue") + private @Nullable Void stop(@Nullable IDevice d, @NotNull Object device) { + if (d == null) { + throw new ErrorDialogException("Unable to stop " + device, + "An error occurred stopping " + device + ". To stop the device, try manually closing the " + device + + " emulator window."); + } + + myGetConsole.apply(d).kill(); + return null; + } + + @VisibleForTesting + static final class SetAllOnline implements FutureCallback<Object> { private final @NotNull VirtualDeviceTableModel myModel; - private SetAllOnline(@NotNull VirtualDeviceTableModel model) { + @VisibleForTesting + SetAllOnline(@NotNull VirtualDeviceTableModel model) { myModel = model; } @Override - public void onSuccess(@NotNull Object object) { + public void onSuccess(@Nullable Object object) { // The launch succeeded. Rely on the VirtualDeviceChangeListener, which calls VirtualDeviceTableModel::setAllOnline, to transition the // device state from VirtualDevice.State.LAUNCHING to VirtualDevice.State.LAUNCHED. Likewise for STOPPING and STOPPED. } @@ -353,6 +411,8 @@ final class VirtualDeviceTableModel extends AbstractTableModel { // The launch failed. Manually transition the state from LAUNCHING to whatever the AvdManagerConnection reports (STOPPED, presumably). // Likewise for STOPPING. myModel.setAllOnline(); + + myModel.myShowErrorDialog.accept(throwable, myModel.myProject); } } } diff --git a/device-manager/testSrc/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModelTest.java b/device-manager/testSrc/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModelTest.java index 46476f2063f..3e72844fb0a 100644 --- a/device-manager/testSrc/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModelTest.java +++ b/device-manager/testSrc/com/android/tools/idea/devicemanager/virtualtab/VirtualDeviceTableModelTest.java @@ -17,26 +17,33 @@ package com.android.tools.idea.devicemanager.virtualtab; import static org.junit.Assert.assertEquals; +import com.android.ddmlib.EmulatorConsole; import com.android.ddmlib.IDevice; import com.android.sdklib.AndroidVersion; import com.android.sdklib.internal.avd.AvdInfo; import com.android.tools.idea.avdmanager.AvdManagerConnection; import com.android.tools.idea.devicemanager.CountDownLatchAssert; import com.android.tools.idea.devicemanager.CountDownLatchFutureCallback; +import com.android.tools.idea.devicemanager.DeviceManagerAndroidDebugBridge; import com.android.tools.idea.devicemanager.Key; +import com.android.tools.idea.devicemanager.virtualtab.VirtualDeviceTableModel.SetAllOnline; import com.android.tools.idea.testing.swing.TableModelEventArgumentMatcher; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.intellij.openapi.project.Project; import java.util.Collection; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.function.BiConsumer; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableModel; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -50,13 +57,17 @@ public final class VirtualDeviceTableModelTest { @Test public void setAllOnline() throws InterruptedException { // Arrange - Mockito.when(myConnection.isAvdRunning(myAvd)).thenReturn(true); CountDownLatch latch = new CountDownLatch(1); + Mockito.when(myConnection.isAvdRunning(myAvd)).thenReturn(true); VirtualDeviceTableModel model = new VirtualDeviceTableModel(null, myDevices, + (m, key) -> newSetOnline(m, key, latch), () -> myConnection, - (m, key) -> newSetOnline(m, key, latch)); + SetAllOnline::new, + new DeviceManagerAndroidDebugBridge(), + EmulatorConsole::getConsole, + VirtualTabMessages::showErrorDialog); model.addTableModelListener(myListener); @@ -81,7 +92,15 @@ public final class VirtualDeviceTableModelTest { // Arrange Mockito.when(myConnection.startAvd(null, myAvd)).thenReturn(Futures.immediateFuture(Mockito.mock(IDevice.class))); - TableModel model = new VirtualDeviceTableModel(null, myDevices, () -> myConnection, VirtualDeviceTableModel::newSetOnline); + TableModel model = new VirtualDeviceTableModel(null, + myDevices, + VirtualDeviceTableModel::newSetOnline, + () -> myConnection, + SetAllOnline::new, + new DeviceManagerAndroidDebugBridge(), + EmulatorConsole::getConsole, + VirtualTabMessages::showErrorDialog); + model.addTableModelListener(myListener); // Act @@ -107,10 +126,22 @@ public final class VirtualDeviceTableModelTest { @Test public void setValueAtStartAvdFailed() throws Exception { // Arrange - Mockito.when(myConnection.startAvd(null, myAvd)).thenReturn(Futures.immediateFailedFuture(new RuntimeException())); CountDownLatch latch = new CountDownLatch(1); + Throwable throwable = new RuntimeException(); + Mockito.when(myConnection.startAvd(null, myAvd)).thenReturn(Futures.immediateFailedFuture(throwable)); + + @SuppressWarnings("unchecked") + BiConsumer<Throwable, Project> showErrorDialog = Mockito.mock(BiConsumer.class); + + TableModel model = new VirtualDeviceTableModel(null, + myDevices, + (m, key) -> newSetOnline(m, key, latch), + () -> myConnection, + SetAllOnline::new, + new DeviceManagerAndroidDebugBridge(), + EmulatorConsole::getConsole, + showErrorDialog); - TableModel model = new VirtualDeviceTableModel(null, myDevices, () -> myConnection, (m, key) -> newSetOnline(m, key, latch)); model.addTableModelListener(myListener); // Act @@ -125,5 +156,120 @@ public final class VirtualDeviceTableModelTest { Mockito.verify(myListener).tableChanged(ArgumentMatchers.argThat(new TableModelEventArgumentMatcher(event))); Mockito.verify(myListener).tableChanged(ArgumentMatchers.argThat(new TableModelEventArgumentMatcher(new TableModelEvent(model, 0)))); + + Mockito.verify(showErrorDialog).accept(throwable, null); + } + + @Test + public void setValueAtStopFailed() throws Exception { + // Arrange + CountDownLatch latch = new CountDownLatch(1); + Mockito.when(myConnection.isAvdRunning(myAvd)).thenReturn(true); + + DeviceManagerAndroidDebugBridge bridge = Mockito.mock(DeviceManagerAndroidDebugBridge.class); + Mockito.when(bridge.findDevice(null, TestVirtualDevices.newKey("Pixel_5_API_31"))).thenReturn(Futures.immediateFuture(null)); + + @SuppressWarnings("unchecked") + BiConsumer<Throwable, Project> showErrorDialog = Mockito.mock(BiConsumer.class); + + TableModel model = new VirtualDeviceTableModel(null, + List.of(TestVirtualDevices.onlinePixel5Api31(myAvd)), + (m, key) -> newSetOnline(m, key, latch), + () -> myConnection, + SetAllOnline::new, + bridge, + EmulatorConsole::getConsole, + showErrorDialog); + + model.addTableModelListener(myListener); + + // Act + model.setValueAt(VirtualDevice.State.STOPPING, 0, VirtualDeviceTableModel.LAUNCH_OR_STOP_MODEL_COLUMN_INDEX); + + // Assert + CountDownLatchAssert.await(latch); + + assertEquals(TestVirtualDevices.onlinePixel5Api31(myAvd), model.getValueAt(0, VirtualDeviceTableModel.DEVICE_MODEL_COLUMN_INDEX)); + + TableModelEvent event = new TableModelEvent(model, 0, 0, VirtualDeviceTableModel.LAUNCH_OR_STOP_MODEL_COLUMN_INDEX); + Mockito.verify(myListener).tableChanged(ArgumentMatchers.argThat(new TableModelEventArgumentMatcher(event))); + + Mockito.verify(myListener).tableChanged(ArgumentMatchers.argThat(new TableModelEventArgumentMatcher(new TableModelEvent(model, 0)))); + + ErrorDialogException exception = new ErrorDialogException( + "Unable to stop Pixel 5 API 31", + "An error occurred stopping Pixel 5 API 31. To stop the device, try manually closing the Pixel 5 API 31 emulator window."); + + ArgumentMatcher<Throwable> matcher = new ErrorDialogExceptionArgumentMatcher(exception); + Mockito.verify(showErrorDialog).accept(ArgumentMatchers.argThat(matcher), ArgumentMatchers.isNull()); + } + + private static final class ErrorDialogExceptionArgumentMatcher implements ArgumentMatcher<Throwable> { + private final @NotNull ErrorDialogException myExpected; + + private ErrorDialogExceptionArgumentMatcher(@NotNull ErrorDialogException expected) { + myExpected = expected; + } + + @Override + public boolean matches(@NotNull Throwable actual) { + if (!(actual instanceof ErrorDialogException)) { + return false; + } + + ErrorDialogException exception = (ErrorDialogException)actual; + return myExpected.getTitle().equals(exception.getTitle()) && myExpected.getMessage().equals(exception.getMessage()); + } + } + + @Test + public void setValueAtStopSucceeded() throws Exception { + // Arrange + CountDownLatch latch = new CountDownLatch(1); + Key key = TestVirtualDevices.newKey("Pixel_5_API_31"); + + DeviceManagerAndroidDebugBridge bridge = Mockito.mock(DeviceManagerAndroidDebugBridge.class); + Mockito.when(bridge.findDevice(null, key)).thenReturn(Futures.immediateFuture(Mockito.mock(IDevice.class))); + + EmulatorConsole console = Mockito.mock(EmulatorConsole.class); + + TableModel model = new VirtualDeviceTableModel(null, + List.of(TestVirtualDevices.onlinePixel5Api31(myAvd)), + VirtualDeviceTableModel::newSetOnline, + AvdManagerConnection::getDefaultAvdManagerConnection, + m -> newSetAllOnline(m, latch), + bridge, + device -> console, + VirtualTabMessages::showErrorDialog); + + model.addTableModelListener(myListener); + + // Act + model.setValueAt(VirtualDevice.State.STOPPING, 0, VirtualDeviceTableModel.LAUNCH_OR_STOP_MODEL_COLUMN_INDEX); + + // Assert + CountDownLatchAssert.await(latch); + + Object device = new VirtualDevice.Builder() + .setKey(key) + .setName("Pixel 5 API 31") + .setTarget("Android 12.0 Google APIs") + .setCpuArchitecture("x86_64") + .setAndroidVersion(new AndroidVersion(31)) + .setState(VirtualDevice.State.STOPPING) + .setAvdInfo(myAvd) + .build(); + + assertEquals(device, model.getValueAt(0, VirtualDeviceTableModel.DEVICE_MODEL_COLUMN_INDEX)); + + TableModelEvent event = new TableModelEvent(model, 0, 0, VirtualDeviceTableModel.LAUNCH_OR_STOP_MODEL_COLUMN_INDEX); + Mockito.verify(myListener).tableChanged(ArgumentMatchers.argThat(new TableModelEventArgumentMatcher(event))); + + Mockito.verify(console).kill(); + } + + private static @NotNull FutureCallback<@Nullable Object> newSetAllOnline(@NotNull VirtualDeviceTableModel model, + @NotNull CountDownLatch latch) { + return new CountDownLatchFutureCallback<>(new SetAllOnline(model), latch); } } |