/* * 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 org.jetbrains.android.run; 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.ddms.DeviceRenderer; import com.android.tools.idea.model.AndroidModuleInfo; import com.android.tools.idea.model.ManifestInfo; import com.android.tools.idea.run.LaunchCompatibility; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.util.Condition; 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.containers.HashSet; import gnu.trove.TIntArrayList; import org.jetbrains.android.dom.AndroidAttributeValue; import org.jetbrains.android.dom.manifest.UsesFeature; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import static com.intellij.openapi.util.text.StringUtil.capitalize; /** * @author Eugene.Kudelevsky */ public class DeviceChooser implements Disposable { private static final String[] COLUMN_TITLES = new String[]{"Device", "Serial Number", "State", "Compatible"}; private static final int DEVICE_NAME_COLUMN_INDEX = 0; private static final int SERIAL_COLUMN_INDEX = 1; private static final int DEVICE_STATE_COLUMN_INDEX = 2; private static final int COMPATIBILITY_COLUMN_INDEX = 3; private static final int REFRESH_INTERVAL_MS = 500; public static final IDevice[] EMPTY_DEVICE_ARRAY = new IDevice[0]; private final List<DeviceChooserListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private final Alarm myRefreshingAlarm; private final AndroidDebugBridge myBridge; private volatile boolean myProcessSelectionFlag = true; /** The current list of devices that is displayed in the table. */ private IDevice[] myDisplayedDevices = EMPTY_DEVICE_ARRAY; /** * The current list of devices obtained from the debug bridge. This is updated in a background thread. * If it is different than {@link #myDisplayedDevices}, then a {@link #refreshTable} invocation in the EDT thread * will update the displayed list to match the detected list. */ private AtomicReference<IDevice[]> myDetectedDevicesRef = new AtomicReference<IDevice[]>(EMPTY_DEVICE_ARRAY); private JComponent myPanel; private JBTable myDeviceTable; private final AndroidFacet myFacet; private final Condition<IDevice> myFilter; private final AndroidVersion myMinSdkVersion; private final IAndroidTarget myProjectTarget; private final EnumSet<IDevice.HardwareFeature> myRequiredHardwareFeatures; private int[] mySelectedRows; public DeviceChooser(boolean multipleSelection, @NotNull final Action okAction, @NotNull AndroidFacet facet, @NotNull IAndroidTarget projectTarget, @Nullable Condition<IDevice> filter) { myFacet = facet; myFilter = filter; myMinSdkVersion = AndroidModuleInfo.get(facet).getRuntimeMinSdkVersion(); myProjectTarget = projectTarget; myRequiredHardwareFeatures = getRequiredHardwareFeatures(ManifestInfo.get(facet.getModule(), true).getRequiredFeatures()); myDeviceTable = new JBTable(); myPanel = ScrollPaneFactory.createScrollPane(myDeviceTable); myPanel.setPreferredSize(new Dimension(450, 220)); myDeviceTable.setModel(new MyDeviceTableModel(EMPTY_DEVICE_ARRAY)); 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()); myDeviceTable.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER && okAction.isEnabled()) { okAction.actionPerformed(null); } } }); setColumnWidth(myDeviceTable, DEVICE_NAME_COLUMN_INDEX, "Samsung Galaxy Nexus Android 4.1 (API 17)"); setColumnWidth(myDeviceTable, SERIAL_COLUMN_INDEX, "0000-0000-00000"); setColumnWidth(myDeviceTable, DEVICE_STATE_COLUMN_INDEX, "offline"); setColumnWidth(myDeviceTable, COMPATIBILITY_COLUMN_INDEX, "yes"); // 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); myRefreshingAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this); myBridge = AndroidSdkUtils.getDebugBridge(myFacet.getModule().getProject()); } private static EnumSet<IDevice.HardwareFeature> getRequiredHardwareFeatures(List<UsesFeature> requiredFeatures) { // Currently, this method is hardcoded to only search if the list of required features includes a watch. // We may not want to search the device for every possible feature, but only a small subset of important // features, starting with hardware type watch.. for (UsesFeature feature : requiredFeatures) { AndroidAttributeValue<String> name = feature.getName(); if (name != null && UsesFeature.HARDWARE_TYPE_WATCH.equals(name.getStringValue())) { return EnumSet.of(IDevice.HardwareFeature.WATCH); } } return EnumSet.noneOf(IDevice.HardwareFeature.class); } private void setColumnWidth(JBTable deviceTable, int columnIndex, String sampleText) { int width = getWidth(deviceTable, sampleText); deviceTable.getColumnModel().getColumn(columnIndex).setPreferredWidth(width); } private 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); } addUpdatingRequest(); } private final Runnable myUpdateRequest = new Runnable() { @Override public void run() { updateTable(); addUpdatingRequest(); } }; private void addUpdatingRequest() { if (myRefreshingAlarm.isDisposed()) { return; } myRefreshingAlarm.cancelAllRequests(); myRefreshingAlarm.addRequest(myUpdateRequest, REFRESH_INTERVAL_MS); } private void resetSelection(@NotNull String[] selectedSerials) { MyDeviceTableModel model = (MyDeviceTableModel)myDeviceTable.getModel(); Set<String> selectedSerialsSet = new HashSet<String>(); 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); } } } void updateTable() { IDevice[] devices = myBridge != null ? getFilteredDevices(myBridge) : EMPTY_DEVICE_ARRAY; 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; } } }); } if (!Arrays.equals(myDisplayedDevices, devices)) { myDetectedDevicesRef.set(devices); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { refreshTable(); } }, ModalityState.stateForComponent(myDeviceTable)); } } private void refreshTable() { IDevice[] devices = myDetectedDevicesRef.get(); myDisplayedDevices = devices; final IDevice[] selectedDevices = getSelectedDevices(); 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.size() == 0 && 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 myDetectedDevicesRef.get().length > 0; } public JComponent getPreferredFocusComponent() { return myDeviceTable; } @Nullable public JComponent getPanel() { return myPanel; } @NotNull public IDevice[] getSelectedDevices() { int[] rows = mySelectedRows != null ? mySelectedRows : myDeviceTable.getSelectedRows(); List<IDevice> result = new ArrayList<IDevice>(); for (int row : rows) { if (row >= 0) { Object serial = myDeviceTable.getValueAt(row, SERIAL_COLUMN_INDEX); final AndroidDebugBridge bridge = AndroidSdkUtils.getDebugBridge(myFacet.getModule().getProject()); if (bridge == null) { return EMPTY_DEVICE_ARRAY; } IDevice[] devices = getFilteredDevices(bridge); for (IDevice device : devices) { if (device.getSerialNumber().equals(serial.toString())) { result.add(device); break; } } } } return result.toArray(new IDevice[result.size()]); } @NotNull private IDevice[] getFilteredDevices(AndroidDebugBridge bridge) { final IDevice[] devices = bridge.getDevices(); if (devices.length == 0 || myFilter == null) { return devices; } final List<IDevice> filteredDevices = new ArrayList<IDevice>(); for (IDevice device : devices) { if (myFilter.value(device)) { filteredDevices.add(device); } } return filteredDevices.toArray(new IDevice[filteredDevices.size()]); } public void finish() { mySelectedRows = myDeviceTable.getSelectedRows(); } @Override public void dispose() { } 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(state.name().toLowerCase()) : ""; } public void fireSelectedDevicesChanged() { for (DeviceChooserListener listener : myListeners) { listener.selectedDevicesChanged(); } } public void addListener(@NotNull DeviceChooserListener listener) { myListeners.add(listener); } 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: return LaunchCompatibility.canRunOnDevice(myMinSdkVersion, myProjectTarget, myRequiredHardwareFeatures, device, null); } 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); } } } } }