| /* |
| * Copyright 2000-2010 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.idea.run; |
| |
| import static com.intellij.openapi.util.text.StringUtil.capitalize; |
| |
| import com.android.ddmlib.AndroidDebugBridge; |
| import com.android.ddmlib.IDevice; |
| import com.android.sdklib.AndroidVersion; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.tools.idea.avdmanager.AvdManagerUtils; |
| import com.android.tools.idea.ddms.DeviceNameProperties; |
| import com.android.tools.idea.ddms.DeviceNamePropertiesFetcher; |
| import com.android.tools.idea.ddms.DeviceRenderer; |
| import com.android.tools.idea.gradle.project.model.AndroidModuleModel; |
| import com.android.tools.idea.model.AndroidModuleInfo; |
| import com.google.common.base.Predicate; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.ide.CopyPasteManager; |
| import com.intellij.openapi.ui.JBPopupMenu; |
| import com.intellij.openapi.ui.ValidationInfo; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.ui.ColoredTableCellRenderer; |
| import com.intellij.ui.DoubleClickListener; |
| import com.intellij.ui.ScrollPaneFactory; |
| import com.intellij.ui.SimpleTextAttributes; |
| import com.intellij.ui.table.JBTable; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.ThreeState; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.ui.JBUI; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.util.ui.update.MergingUpdateQueue; |
| import com.intellij.util.ui.update.Update; |
| import gnu.trove.TIntArrayList; |
| import java.awt.FontMetrics; |
| import java.awt.datatransfer.StringSelection; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.MouseAdapter; |
| import java.awt.event.MouseEvent; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import javax.swing.AbstractAction; |
| import javax.swing.Action; |
| import javax.swing.JComponent; |
| import javax.swing.JTable; |
| import javax.swing.ListSelectionModel; |
| import javax.swing.event.ListSelectionEvent; |
| import javax.swing.event.ListSelectionListener; |
| import javax.swing.table.AbstractTableModel; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| public class DeviceChooser implements Disposable, AndroidDebugBridge.IDebugBridgeChangeListener, AndroidDebugBridge.IDeviceChangeListener { |
| private final static int UPDATE_DELAY_MILLIS = 250; |
| private static final String[] COLUMN_TITLES = new String[]{"Device", "State", "Compatible", "Serial Number"}; |
| private static final int DEVICE_NAME_COLUMN_INDEX = 0; |
| private static final int DEVICE_STATE_COLUMN_INDEX = 1; |
| private static final int COMPATIBILITY_COLUMN_INDEX = 2; |
| private static final int SERIAL_COLUMN_INDEX = 3; |
| |
| public static final IDevice[] EMPTY_DEVICE_ARRAY = new IDevice[0]; |
| |
| private final List<DeviceChooserListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final MergingUpdateQueue myUpdateQueue; |
| |
| private volatile boolean myProcessSelectionFlag = true; |
| |
| private final JComponent myPanel; |
| private final JBTable myDeviceTable; |
| |
| private final Predicate<IDevice> myFilter; |
| private final ListenableFuture<AndroidVersion> myMinSdkVersion; |
| private final AndroidFacet myFacet; |
| private final IAndroidTarget myProjectTarget; |
| private final Set<String> mySupportedAbis; |
| |
| private int[] mySelectedRows; |
| private final AtomicBoolean myDevicesDetected = new AtomicBoolean(); |
| |
| public DeviceChooser(boolean multipleSelection, |
| @NotNull final Action okAction, |
| @NotNull AndroidFacet facet, |
| @NotNull IAndroidTarget projectTarget, |
| @Nullable Predicate<IDevice> filter) { |
| myFacet = facet; |
| myFilter = filter; |
| myMinSdkVersion = AndroidModuleInfo.getInstance(facet).getRuntimeMinSdkVersion(); |
| myProjectTarget = projectTarget; |
| AndroidModuleModel androidModuleModel = AndroidModuleModel.get(facet); |
| mySupportedAbis = androidModuleModel != null ? |
| androidModuleModel.getSelectedVariant().getMainArtifact().getAbiFilters() : |
| Collections.emptySet(); |
| |
| myDeviceTable = new JBTable(); |
| myPanel = ScrollPaneFactory.createScrollPane(myDeviceTable); |
| myPanel.setPreferredSize(JBUI.size(550, 220)); |
| |
| MyDeviceTableModel tableModel = new MyDeviceTableModel(EMPTY_DEVICE_ARRAY); |
| myDeviceTable.setModel(tableModel); |
| myDeviceTable |
| .setSelectionMode(multipleSelection ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION); |
| myDeviceTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { |
| @Override |
| public void valueChanged(ListSelectionEvent e) { |
| if (myProcessSelectionFlag) { |
| fireSelectedDevicesChanged(); |
| } |
| } |
| }); |
| new DoubleClickListener() { |
| @Override |
| protected boolean onDoubleClick(MouseEvent e) { |
| if (myDeviceTable.isEnabled() && okAction.isEnabled()) { |
| okAction.actionPerformed(null); |
| return true; |
| } |
| return false; |
| } |
| }.installOn(myDeviceTable); |
| |
| myDeviceTable.setDefaultRenderer(LaunchCompatibility.class, new LaunchCompatibilityRenderer()); |
| myDeviceTable.setDefaultRenderer(IDevice.class, new DeviceRenderer.DeviceNameRenderer( |
| AvdManagerUtils.getAvdManagerSilently(facet), |
| new DeviceNamePropertiesFetcher(new FutureCallback<DeviceNameProperties>() { |
| @Override |
| public void onSuccess(@Nullable DeviceNameProperties result) { |
| updateTable(); |
| } |
| |
| @Override |
| public void onFailure(@NotNull Throwable t) { |
| Logger.getInstance(DeviceChooser.class).warn("Error retrieving device name properties", t); |
| } |
| }, this))); |
| myDeviceTable.addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| if (e.getKeyCode() == KeyEvent.VK_ENTER && okAction.isEnabled()) { |
| okAction.actionPerformed(null); |
| } |
| } |
| }); |
| myDeviceTable.addMouseListener(new MouseAdapter() { |
| @Override |
| public void mouseReleased(MouseEvent e) { |
| if (e.getButton() == MouseEvent.BUTTON3) { |
| int i = myDeviceTable.rowAtPoint(e.getPoint()); |
| if (i >= 0) { |
| Object serial = myDeviceTable.getValueAt(i, SERIAL_COLUMN_INDEX); |
| final String serialString = serial.toString(); |
| // Add a menu to copy the serial key. |
| JBPopupMenu popupMenu = new JBPopupMenu(); |
| Action action = new AbstractAction("Copy Serial Number") { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| CopyPasteManager.getInstance().setContents(new StringSelection(serialString)); |
| } |
| }; |
| popupMenu.add(action); |
| popupMenu.show(e.getComponent(), e.getX(), e.getY()); |
| } |
| } |
| super.mouseReleased(e); |
| } |
| }); |
| setColumnWidth(myDeviceTable, DEVICE_NAME_COLUMN_INDEX, "Samsung Galaxy Nexus Android 4.1 (API 17)"); |
| setColumnWidth(myDeviceTable, DEVICE_STATE_COLUMN_INDEX, "offline"); |
| setColumnWidth(myDeviceTable, COMPATIBILITY_COLUMN_INDEX, "Compatible"); |
| setColumnWidth(myDeviceTable, SERIAL_COLUMN_INDEX, "123456"); |
| |
| // Do not recreate columns on every model update - this should help maintain the column sizes set above |
| myDeviceTable.setAutoCreateColumnsFromModel(false); |
| |
| // Allow sorting by columns (in lexicographic order) |
| myDeviceTable.setAutoCreateRowSorter(true); |
| |
| // the device change notifications from adb can sometimes be noisy (esp. when a device is [dis]connected) |
| // we use this merging queue to collapse multiple updates to one |
| myUpdateQueue = new MergingUpdateQueue("android.device.chooser", UPDATE_DELAY_MILLIS, true, null, this, null, |
| Alarm.ThreadToUse.POOLED_THREAD); |
| |
| AndroidDebugBridge.addDebugBridgeChangeListener(this); |
| AndroidDebugBridge.addDeviceChangeListener(this); |
| |
| myMinSdkVersion.addListener(() -> tableModel.fireTableDataChanged(), ApplicationManager.getApplication()::invokeLater); |
| } |
| |
| private static void setColumnWidth(JBTable deviceTable, int columnIndex, String sampleText) { |
| int width = getWidth(deviceTable, sampleText); |
| deviceTable.getColumnModel().getColumn(columnIndex).setPreferredWidth(width); |
| } |
| |
| private static int getWidth(JBTable deviceTable, String sampleText) { |
| FontMetrics metrics = deviceTable.getFontMetrics(deviceTable.getFont()); |
| return metrics.stringWidth(sampleText); |
| } |
| |
| public void init(@Nullable String[] selectedSerials) { |
| updateTable(); |
| if (selectedSerials != null) { |
| resetSelection(selectedSerials); |
| } |
| } |
| |
| private void resetSelection(@NotNull String[] selectedSerials) { |
| MyDeviceTableModel model = (MyDeviceTableModel)myDeviceTable.getModel(); |
| Set<String> selectedSerialsSet = new HashSet<>(); |
| Collections.addAll(selectedSerialsSet, selectedSerials); |
| IDevice[] myDevices = model.myDevices; |
| ListSelectionModel selectionModel = myDeviceTable.getSelectionModel(); |
| boolean cleared = false; |
| |
| for (int i = 0, n = myDevices.length; i < n; i++) { |
| String serialNumber = myDevices[i].getSerialNumber(); |
| if (selectedSerialsSet.contains(serialNumber)) { |
| if (!cleared) { |
| selectionModel.clearSelection(); |
| cleared = true; |
| } |
| selectionModel.addSelectionInterval(i, i); |
| } |
| } |
| } |
| |
| private void updateTable() { |
| final IDevice[] devices = getFilteredDevices(); |
| if (devices.length > 1) { |
| // sort by API level |
| Arrays.sort(devices, new Comparator<IDevice>() { |
| @Override |
| public int compare(IDevice device1, IDevice device2) { |
| int apiLevel1 = safeGetApiLevel(device1); |
| int apiLevel2 = safeGetApiLevel(device2); |
| return apiLevel2 - apiLevel1; |
| } |
| |
| private int safeGetApiLevel(IDevice device) { |
| try { |
| String s = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); |
| return StringUtil.isNotEmpty(s) ? Integer.parseInt(s) : 0; |
| } |
| catch (Exception e) { |
| return 0; |
| } |
| } |
| }); |
| } |
| |
| UIUtil.invokeLaterIfNeeded(() -> { |
| myDevicesDetected.set(devices.length > 0); |
| refreshTable(devices); |
| }); |
| } |
| |
| private void refreshTable(IDevice[] devices) { |
| final IDevice[] selectedDevices = getSelectedDevices(false); |
| final TIntArrayList selectedRows = new TIntArrayList(); |
| for (int i = 0; i < devices.length; i++) { |
| if (ArrayUtil.indexOf(selectedDevices, devices[i]) >= 0) { |
| selectedRows.add(i); |
| } |
| } |
| |
| myProcessSelectionFlag = false; |
| myDeviceTable.setModel(new MyDeviceTableModel(devices)); |
| if (selectedRows.isEmpty() && devices.length > 0) { |
| myDeviceTable.getSelectionModel().setSelectionInterval(0, 0); |
| } |
| for (int selectedRow : selectedRows.toNativeArray()) { |
| if (selectedRow < devices.length) { |
| myDeviceTable.getSelectionModel().addSelectionInterval(selectedRow, selectedRow); |
| } |
| } |
| fireSelectedDevicesChanged(); |
| myProcessSelectionFlag = true; |
| } |
| |
| public boolean hasDevices() { |
| return myDevicesDetected.get(); |
| } |
| |
| public JComponent getPreferredFocusComponent() { |
| return myDeviceTable; |
| } |
| |
| @Nullable |
| public JComponent getPanel() { |
| return myPanel; |
| } |
| |
| @Nullable |
| public ValidationInfo doValidate() { |
| if (!myDeviceTable.isEnabled()) { |
| return null; |
| } |
| |
| int[] rows = mySelectedRows != null ? mySelectedRows : myDeviceTable.getSelectedRows(); |
| boolean hasIncompatible = false; |
| boolean hasCompatible = false; |
| for (int row : rows) { |
| if (!isRowCompatible(row)) { |
| hasIncompatible = true; |
| } |
| else { |
| hasCompatible = true; |
| } |
| } |
| if (!hasIncompatible) { |
| return null; |
| } |
| String message; |
| if (hasCompatible) { |
| message = "At least one of the selected devices is incompatible. Will only install on compatible devices."; |
| } |
| else { |
| String devicesAre = rows.length > 1 ? "devices are" : "device is"; |
| message = "The selected " + devicesAre + " incompatible."; |
| } |
| return new ValidationInfo(message); |
| } |
| |
| @NotNull |
| public IDevice[] getSelectedDevices() { |
| return getSelectedDevices(true); |
| } |
| |
| @NotNull |
| private IDevice[] getSelectedDevices(boolean onlyCompatible) { |
| int[] rows = mySelectedRows != null ? mySelectedRows : myDeviceTable.getSelectedRows(); |
| List<IDevice> result = new ArrayList<>(); |
| for (int row : rows) { |
| if (row >= 0) { |
| if (onlyCompatible && !isRowCompatible(row)) { |
| continue; |
| } |
| Object serial = myDeviceTable.getValueAt(row, SERIAL_COLUMN_INDEX); |
| IDevice[] devices = getFilteredDevices(); |
| for (IDevice device : devices) { |
| if (device.getSerialNumber().equals(serial.toString())) { |
| result.add(device); |
| break; |
| } |
| } |
| } |
| } |
| return result.toArray(new IDevice[0]); |
| } |
| |
| @NotNull |
| private IDevice[] getFilteredDevices() { |
| AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); |
| if (bridge == null || !bridge.isConnected()) { |
| return EMPTY_DEVICE_ARRAY; |
| } |
| |
| final List<IDevice> filteredDevices = new ArrayList<>(); |
| for (IDevice device : bridge.getDevices()) { |
| if (myFilter == null || myFilter.apply(device)) { |
| filteredDevices.add(device); |
| } |
| } |
| |
| return filteredDevices.toArray(new IDevice[0]); |
| } |
| |
| private boolean isRowCompatible(int row) { |
| // Use the value already computed in the table to avoid having to compute it again. |
| Object compatibility = myDeviceTable.getValueAt(row, COMPATIBILITY_COLUMN_INDEX); |
| return compatibility instanceof LaunchCompatibility && ((LaunchCompatibility)compatibility).isCompatible() != ThreeState.NO; |
| } |
| |
| public void finish() { |
| mySelectedRows = myDeviceTable.getSelectedRows(); |
| } |
| |
| @Override |
| public void dispose() { |
| AndroidDebugBridge.removeDebugBridgeChangeListener(this); |
| AndroidDebugBridge.removeDeviceChangeListener(this); |
| } |
| |
| public void setEnabled(boolean enabled) { |
| myDeviceTable.setEnabled(enabled); |
| } |
| |
| @NotNull |
| private static String getDeviceState(@NotNull IDevice device) { |
| IDevice.DeviceState state = device.getState(); |
| return state != null ? capitalize(StringUtil.toLowerCase(state.name())) : ""; |
| } |
| |
| private void fireSelectedDevicesChanged() { |
| for (DeviceChooserListener listener : myListeners) { |
| listener.selectedDevicesChanged(); |
| } |
| } |
| |
| public void addListener(@NotNull DeviceChooserListener listener) { |
| myListeners.add(listener); |
| } |
| |
| @Override |
| public void bridgeChanged(AndroidDebugBridge bridge) { |
| postUpdate(); |
| } |
| |
| @Override |
| public void deviceConnected(@NotNull IDevice device) { |
| postUpdate(); |
| } |
| |
| @Override |
| public void deviceDisconnected(@NotNull IDevice device) { |
| postUpdate(); |
| } |
| |
| @Override |
| public void deviceChanged(@NotNull IDevice device, int changeMask) { |
| postUpdate(); |
| } |
| |
| private void postUpdate() { |
| myUpdateQueue.queue(new Update("updateTable") { |
| @Override |
| public void run() { |
| updateTable(); |
| } |
| |
| @Override |
| public boolean canEat(Update update) { |
| return true; |
| } |
| }); |
| } |
| |
| private class MyDeviceTableModel extends AbstractTableModel { |
| private final IDevice[] myDevices; |
| |
| public MyDeviceTableModel(IDevice[] devices) { |
| myDevices = devices; |
| } |
| |
| @Override |
| public String getColumnName(int column) { |
| return COLUMN_TITLES[column]; |
| } |
| |
| @Override |
| public int getRowCount() { |
| return myDevices.length; |
| } |
| |
| @Override |
| public int getColumnCount() { |
| return COLUMN_TITLES.length; |
| } |
| |
| @Override |
| @Nullable |
| public Object getValueAt(int rowIndex, int columnIndex) { |
| if (rowIndex >= myDevices.length) { |
| return null; |
| } |
| IDevice device = myDevices[rowIndex]; |
| switch (columnIndex) { |
| case DEVICE_NAME_COLUMN_INDEX: |
| return device; |
| case SERIAL_COLUMN_INDEX: |
| return device.getSerialNumber(); |
| case DEVICE_STATE_COLUMN_INDEX: |
| return getDeviceState(device); |
| case COMPATIBILITY_COLUMN_INDEX: |
| // This value is also used in the method isRowCompatible(). Update that if there's a change here. |
| AndroidDevice connectedDevice = new ConnectedAndroidDevice(device, null); |
| try { |
| return myMinSdkVersion.isDone() ? connectedDevice |
| .canRun(myMinSdkVersion.get(), myProjectTarget, myFacet, LaunchCompatibilityCheckerImpl::getRequiredHardwareFeatures, |
| mySupportedAbis) : false; |
| } |
| catch (InterruptedException | ExecutionException e) { |
| return false; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public Class<?> getColumnClass(int columnIndex) { |
| if (columnIndex == COMPATIBILITY_COLUMN_INDEX) { |
| return LaunchCompatibility.class; |
| } |
| else if (columnIndex == DEVICE_NAME_COLUMN_INDEX) { |
| return IDevice.class; |
| } |
| else { |
| return String.class; |
| } |
| } |
| } |
| |
| private static class LaunchCompatibilityRenderer extends ColoredTableCellRenderer { |
| @Override |
| protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { |
| if (!(value instanceof LaunchCompatibility)) { |
| return; |
| } |
| |
| LaunchCompatibility compatibility = (LaunchCompatibility)value; |
| ThreeState compatible = compatibility.isCompatible(); |
| if (compatible == ThreeState.YES) { |
| append("Yes"); |
| } |
| else { |
| if (compatible == ThreeState.NO) { |
| append("No", SimpleTextAttributes.ERROR_ATTRIBUTES); |
| } |
| else { |
| append("Maybe"); |
| } |
| String reason = compatibility.getReason(); |
| if (reason != null) { |
| append(", "); |
| append(reason); |
| } |
| } |
| } |
| } |
| } |