diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9904c3b..3f5c470a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Refactored `DeviceSelectorAction` and add rich icons to different platform devices (#8475) - Fix DTD freezes when opening projects, and EDT freezes when the theme is changed and opening embedded DevTools (#8477) - Fix `DeviceSelectorAction` `NoSuchElementException` in the toolbar layout (#8515) +- Fix `DeviceSelectorAction`'s concurrent modification exception. (#8550) ## 87.1.0 diff --git a/src/io/flutter/actions/DeviceSelectorAction.java b/src/io/flutter/actions/DeviceSelectorAction.java index 96e8b8a32..6ac9b7e3d 100644 --- a/src/io/flutter/actions/DeviceSelectorAction.java +++ b/src/io/flutter/actions/DeviceSelectorAction.java @@ -13,6 +13,7 @@ import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; @@ -28,6 +29,7 @@ import com.intellij.util.ui.JBUI; import icons.FlutterIcons; import io.flutter.FlutterBundle; +import io.flutter.logging.PluginLogger; import io.flutter.run.FlutterDevice; import io.flutter.run.daemon.DeviceService; import io.flutter.sdk.AndroidEmulatorManager; @@ -43,6 +45,8 @@ import java.util.List; public class DeviceSelectorAction extends AnAction implements CustomComponentAction, DumbAware { + private static final @NotNull Logger LOG = PluginLogger.createLogger(DeviceSelectorAction.class); + private static final Key CUSTOM_COMPONENT_KEY = Key.create("customComponent"); private static final Key ICON_LABEL_KEY = Key.create("iconLabel"); private static final Key TEXT_LABEL_KEY = Key.create("textLabel"); @@ -50,7 +54,7 @@ public class DeviceSelectorAction extends AnAction implements CustomComponentAct private static final @NotNull Icon DEFAULT_DEVICE_ICON = FlutterIcons.Mobile; private static final @NotNull Icon DEFAULT_ARROW_ICON = IconUtil.scale(AllIcons.General.ChevronDown, null, 1.2f); - private final List actions = new ArrayList<>(); + private volatile @NotNull List actions = new ArrayList<>(); private final List knownProjects = Collections.synchronizedList(new ArrayList<>()); private @Nullable SelectDeviceAction selectedDeviceAction; @@ -386,7 +390,11 @@ private static boolean isSelectorVisible(@Nullable Project project) { } private void updateActions(@NotNull Project project, @NotNull Presentation presentation) { - actions.clear(); + final String projectName = project.getName(); + LOG.debug("[" + projectName + "] Building device selector actions"); + + // Create a new list instead of modifying the existing one + final List newActions = new ArrayList<>(); final DeviceService deviceService = DeviceService.getInstance(project); @@ -399,7 +407,8 @@ private void updateActions(@NotNull Project project, @NotNull Presentation prese if (device == null) continue; final SelectDeviceAction deviceAction = new SelectDeviceAction(device, devices); - actions.add(deviceAction); + newActions.add(deviceAction); + LOG.debug("[" + projectName + "] Device action added for " + device); if (Objects.equals(device, selectedDevice)) { selectedDeviceAction = deviceAction; @@ -411,37 +420,42 @@ private void updateActions(@NotNull Project project, @NotNull Presentation prese // Show the 'Open iOS Simulator' action. if (SystemInfo.isMac) { boolean simulatorOpen = false; - for (AnAction action : actions) { + for (AnAction action : newActions) { if (action instanceof SelectDeviceAction deviceAction) { final FlutterDevice device = deviceAction.device; if (device.isIOS() && device.emulator()) { simulatorOpen = true; + break; } } } - - actions.add(new Separator()); - actions.add(new OpenSimulatorAction(!simulatorOpen)); + newActions.add(new Separator()); + newActions.add(new OpenSimulatorAction(!simulatorOpen)); + LOG.debug("[" + projectName + "] 'Open iOS Simulator' action added"); } // Add Open Android emulators actions. final List emulatorActions = OpenEmulatorAction.getEmulatorActions(project); if (emulatorActions != null && !emulatorActions.isEmpty()) { - actions.add(new Separator()); - actions.addAll(emulatorActions); + newActions.add(new Separator()); + newActions.addAll(emulatorActions); + LOG.debug("[" + projectName + "] Emulator action added: " + emulatorActions); } if (!FlutterModuleUtils.hasInternalDartSdkPath(project)) { - actions.add(new Separator()); - actions.add(RestartFlutterDaemonAction.forDeviceSelector()); + newActions.add(new Separator()); + newActions.add(RestartFlutterDaemonAction.forDeviceSelector()); } + // Atomically replace the action list + LOG.debug("[" + projectName + "] Replacing device selector actions"); + this.actions = newActions; + var tracker = ActivityTracker.getInstance(); if (tracker != null) { tracker.inc(); } } - private static class SelectDeviceAction extends AnAction { @NotNull private final FlutterDevice device;