blob: 05faf7aaa9a1c5b739a3745e7cae052b316d418b [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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.avdmanager;
import com.android.sdklib.*;
import com.android.sdklib.devices.Abi;
import com.android.sdklib.repository.MajorRevision;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.IdDisplay;
import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.tools.idea.sdk.SdkState;
import com.android.tools.idea.sdk.remote.RemotePkgInfo;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.android.tools.idea.wizard.DialogWrapperHost;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Comparing;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.TableView;
import com.intellij.util.ui.*;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.util.*;
import java.util.List;
import static com.android.tools.idea.avdmanager.AvdWizardConstants.TV_TAG;
import static com.android.tools.idea.avdmanager.AvdWizardConstants.WEAR_TAG;
/**
* Displays a list of system images currently installed and allows selection of one
*/
public class SystemImageList extends JPanel implements ListSelectionListener {
private final JButton myRefreshButton = new JButton(AllIcons.Actions.Refresh);
private final JBCheckBox myShowRemoteCheckbox = new JBCheckBox("Show downloadable system images", false);
private final JButton myInstallLatestVersionButton = new JButton("Install Latest Version...");
private final Project myProject;
private final JPanel myRemoteStatusPanel = new JPanel(new CardLayout());
private final SdkState mySdkState;
private TableView<SystemImageDescription> myTable = new TableView<SystemImageDescription>();
private ListTableModel<SystemImageDescription> myModel = new ListTableModel<SystemImageDescription>();
private Set<SystemImageSelectionListener> myListeners = Sets.newHashSet();
private Predicate<SystemImageDescription> myFilter;
private static final String ERROR_KEY = "error";
private static final String LOADING_KEY = "loading";
/**
* Components which wish to receive a notification when the user has selected an AVD from this
* table must implement this interface and register themselves through {@link #addSelectionListener(SystemImageSelectionListener)}
*/
public interface SystemImageSelectionListener {
void onSystemImageSelected(@Nullable SystemImageDescription systemImage);
}
public SystemImageList(@Nullable Project project) {
myProject = project;
mySdkState = SdkState.getInstance(AndroidSdkUtils.tryToChooseAndroidSdk());
myModel.setColumnInfos(ourColumnInfos);
myModel.setSortable(true);
myTable.setModelAndUpdateColumns(myModel);
ListSelectionModel selectionModel =
new DefaultListSelectionModel() {
@Override
public void setSelectionInterval(int index0, int index1) {
super.setSelectionInterval(index0, index1);
TableCellEditor editor = myTable.getCellEditor();
if (editor != null) {
editor.cancelCellEditing();
}
myTable.repaint();
possiblySwitchEditors(index0, 0);
}
};
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTable.setSelectionModel(selectionModel);
myTable.setRowSelectionAllowed(true);
myTable.addMouseListener(editorListener);
myTable.addMouseMotionListener(editorListener);
setLayout(new BorderLayout());
add(ScrollPaneFactory.createScrollPane(myTable), BorderLayout.CENTER);
JPanel southPanel = new JPanel(new BorderLayout());
JPanel refreshMessageAndButton = new JPanel(new FlowLayout());
AsyncProcessIcon refreshIcon = new AsyncProcessIcon("refresh images");
JLabel refreshingLabel = new JLabel("Refreshing...");
refreshingLabel.setForeground(JBColor.GRAY);
JPanel refreshPanel = new JPanel(new FlowLayout());
refreshPanel.add(refreshIcon);
refreshPanel.add(refreshingLabel);
refreshPanel.setVisible(false);
myRemoteStatusPanel.add(refreshPanel, LOADING_KEY);
JLabel errorLabel = new JLabel("Error loading remote images");
errorLabel.setForeground(JBColor.GRAY);
JPanel errorPanel = new JPanel(new FlowLayout());
errorPanel.add(errorLabel);
myRemoteStatusPanel.add(errorPanel, ERROR_KEY);
refreshMessageAndButton.add(myRemoteStatusPanel);
refreshMessageAndButton.add(myRefreshButton);
southPanel.add(refreshMessageAndButton, BorderLayout.EAST);
southPanel.add(myShowRemoteCheckbox, BorderLayout.WEST);
myShowRemoteCheckbox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
myModel.fireTableDataChanged();
}
});
myRefreshButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
refreshImages(true);
}
});
myInstallLatestVersionButton.addActionListener(new ActionListener() { // TODO(jbakermalone): actually show this button in the ui
@Override
public void actionPerformed(ActionEvent e) {
installForDevice();
}
});
add(southPanel, BorderLayout.SOUTH);
myTable.getSelectionModel().addListSelectionListener(this);
TableRowSorter<ListTableModel<SystemImageDescription>> sorter =
new TableRowSorter<ListTableModel<SystemImageDescription>>(myModel) {
@Override
public Comparator<?> getComparator(int column) {
if (column == 1) {
// API levels: Sort numerically, but the column is of type String.class since
// it can contain preview codenames as well
return new ApiLevelComparator();
}
// We could consider sorting
return super.getComparator(column);
}
};
sorter.setSortKeys(Collections.singletonList(new RowSorter.SortKey(1, SortOrder.DESCENDING)));
sorter.setRowFilter(new RowFilter<ListTableModel<SystemImageDescription>, Integer>() {
@Override
public boolean include(Entry<? extends ListTableModel<SystemImageDescription>, ? extends Integer> entry) {
return !myModel.getRowValue(entry.getIdentifier()).isRemote() || myShowRemoteCheckbox.isSelected();
}
});
myTable.setRowSorter(sorter);
}
private final MouseAdapter editorListener = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseEntered(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseExited(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseClicked(MouseEvent e) {
possiblySwitchEditors(e);
}
};
private void possiblySwitchEditors(MouseEvent e) {
Point p = e.getPoint();
int row = myTable.rowAtPoint(p);
int col = myTable.columnAtPoint(p);
possiblySwitchEditors(row, col);
}
private void possiblySwitchEditors(int row, int col) {
if (row != myTable.getEditingRow() || col != myTable.getEditingColumn()) {
if (row != -1 && col != -1 && myTable.isCellEditable(row, col)) {
myTable.editCellAt(row, col);
}
}
}
public void refreshImages(final boolean forceRefresh) {
((CardLayout)myRemoteStatusPanel.getLayout()).show(myRemoteStatusPanel, LOADING_KEY);
myRemoteStatusPanel.setVisible(true);
myRefreshButton.setEnabled(false);
final List<SystemImageDescription> items = Lists.newArrayList();
Runnable localComplete = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
items.addAll(getLocalImages());
// Update list in the UI immediately with the locally available system images
updateListModel(items);
if (items.isEmpty()) {
myShowRemoteCheckbox.setSelected(true);
}
}
});
}
};
Runnable remoteComplete = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
List<SystemImageDescription> remotes = getRemoteImages();
if (remotes != null) {
items.addAll(remotes);
updateListModel(items);
myRemoteStatusPanel.setVisible(false);
myRefreshButton.setEnabled(true);
}
else {
myShowRemoteCheckbox.setEnabled(false);
myShowRemoteCheckbox.setSelected(false);
myRemoteStatusPanel.setVisible(false);
}
}
});
}
};
Runnable error = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
((CardLayout)myRemoteStatusPanel.getLayout()).show(myRemoteStatusPanel, ERROR_KEY);
myRefreshButton.setEnabled(true);
}
});
}
};
mySdkState.loadAsync(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, false,
localComplete, remoteComplete, error, forceRefresh);
}
@Nullable
private List<SystemImageDescription> getRemoteImages() {
List<SystemImageDescription> items = Lists.newArrayList();
Set<RemotePkgInfo> infos = mySdkState.getPackages().getNewPkgs();
if (infos.isEmpty()) {
return null;
}
else {
for (RemotePkgInfo info : infos) {
if (info.getPkgDesc().getType().equals(PkgType.PKG_SYS_IMAGE)) {
IAndroidTarget target = findTarget(info);
SystemImageDescription desc = new SystemImageDescription(info.getPkgDesc(), target);
if (myFilter == null || myFilter.apply(desc)) {
items.add(desc);
}
}
}
}
return items;
}
private IAndroidTarget findTarget(RemotePkgInfo info) {
AndroidSdkData data = mySdkState.getSdkData();
assert data != null; // we shouldn't be able to get here without local data being set
IAndroidTarget[] targets = data.getLocalSdk().getTargets();
for (IAndroidTarget target : targets) {
IdDisplay imageVendor = info.getPkgDesc().getVendor();
if ((imageVendor == null && target.isPlatform() ||
imageVendor != null && imageVendor.getId().equals(target.getVendor())) &&
info.getPkgDesc().getAndroidVersion().equals(target.getVersion()))
return target;
}
return null;
}
public void refreshLocalImagesSynchronously() {
myModel.setItems(getLocalImages());
}
private List<SystemImageDescription> getLocalImages() {
List<SystemImageDescription> items = Lists.newArrayList();
AndroidSdkData data = mySdkState.getSdkData();
assert data != null; // we shouldn't be able to get here without local data being set
List<IAndroidTarget> targets = Lists.newArrayList(data.getLocalSdk().getTargets());
for (IAndroidTarget target : targets) {
ISystemImage[] systemImages = target.getSystemImages();
if (systemImages != null) {
for (ISystemImage image : systemImages) {
// If we don't have a filter or this image passes the filter
SystemImageDescription desc = new SystemImageDescription(target, image);
if (myFilter == null || myFilter.apply(desc)) {
items.add(desc);
}
}
}
}
return items;
}
/**
* Shows the given items. May be called from the background thread but will ensure
* that the updates are applied in the UI thread.
*/
private void updateListModel(@NotNull final List<SystemImageDescription> items) {
SystemImageDescription selected = myTable.getSelectedObject();
myModel.setItems(items);
if (selected == null || !items.contains(selected)) {
selectDefaultImage();
}
else {
setSelectedImage(selected);
}
}
public void setFilter(Predicate<SystemImageDescription> filter) {
myFilter = filter;
}
public void addSelectionListener(SystemImageSelectionListener listener) {
myListeners.add(listener);
}
public void selectDefaultImage() {
AndroidVersion maxVersion = null;
SystemImageDescription best = null;
for (SystemImageDescription desc : myModel.getItems()) {
if (!desc.isRemote() && ((maxVersion == null || desc.getVersion().compareTo(maxVersion) > 0) || (desc.getVersion().equals(maxVersion) && desc.getAbiType().equals(Abi.X86.getCpuArch())))) {
best = desc;
maxVersion = best.getVersion();
}
}
setSelectedImage(best);
}
public void setSelectedImage(@Nullable SystemImageDescription selectedImage) {
if (selectedImage != null) {
for (SystemImageDescription listItem : myModel.getItems()) {
if (selectedImage.getVersion().equals(listItem.getVersion()) &&
selectedImage.getAbiType().equals(listItem.getAbiType())) {
myTable.setSelection(ImmutableSet.of(listItem));
return;
}
}
} else {
myTable.clearSelection();
}
}
private void installForDevice() {
int apiLevel = SdkVersionInfo.HIGHEST_KNOWN_STABLE_API;
List<IPkgDesc> requestedPackages = Lists.newArrayListWithCapacity(3);
requestedPackages.add(PkgDesc.Builder.newSysImg(new AndroidVersion(apiLevel, null), SystemImage.DEFAULT_TAG,
Abi.X86.toString(), new MajorRevision(1)).create());
requestedPackages.add(PkgDesc.Builder.newSysImg(new AndroidVersion(apiLevel, null), WEAR_TAG,
Abi.X86.toString(), new MajorRevision(1)).create());
requestedPackages.add(PkgDesc.Builder.newSysImg(new AndroidVersion(apiLevel, null), TV_TAG,
Abi.X86.toString(), new MajorRevision(1)).create());
SdkQuickfixWizard sdkQuickfixWizard = new SdkQuickfixWizard(null, null, requestedPackages);
sdkQuickfixWizard.init();
sdkQuickfixWizard.show();
refreshImages(true);
}
/**
* This class implements the table selection interface and passes the selection events on to its listeners.
* @param e
*/
@Override
public void valueChanged(ListSelectionEvent e) {
SystemImageDescription selected = myTable.getSelectedObject();
for (SystemImageSelectionListener listener : myListeners) {
listener.onSystemImageSelected(selected);
}
}
/**
* List of columns present in our table. Each column is represented by a ColumnInfo which tells the table how to get
* the cell value in that column for a given row item.
*/
private final ColumnInfo[] ourColumnInfos = new ColumnInfo[] {
new SystemImageColumnInfo("Release Name") {
@Nullable
@Override
public String valueOf(SystemImageDescription systemImage) {
String codeName = SdkVersionInfo.getCodeName(systemImage.getVersion().getApiLevel());
String maybeDeprecated = systemImage.getVersion().getApiLevel() < SdkVersionInfo.LOWEST_ACTIVE_API ?
" (Deprecated)" : "";
return codeName == null ? "Unknown" : codeName + maybeDeprecated;
}
},
new SystemImageColumnInfo("API Level", 100) {
@Nullable
@Override
public String valueOf(SystemImageDescription systemImage) {
return systemImage.getVersion().getApiString();
}
@Nullable
@Override
public Comparator<SystemImageDescription> getComparator() {
return new Comparator<SystemImageDescription>() {
@Override
public int compare(SystemImageDescription o1, SystemImageDescription o2) {
return o1.getVersion().getApiLevel() - o2.getVersion().getApiLevel();
}
};
}
},
new SystemImageColumnInfo("ABI", 100) {
@Nullable
@Override
public String valueOf(SystemImageDescription systemImage) {
return systemImage.getAbiType();
}
},
new SystemImageColumnInfo("Target") {
@Nullable
@Override
public String valueOf(SystemImageDescription systemImage) {
IdDisplay tag = systemImage.getTag();
String name = systemImage.getName();
return tag == null || tag.equals(SystemImage.DEFAULT_TAG) ? name : String.format("%1$s - %2$s", name, tag);
}
},
};
/**
* This class extends {@link com.intellij.util.ui.ColumnInfo} in order to pull a string value from a given
* {@link SystemImageDescription}.
* This is the column info used for most of our table, including the Name, Resolution, and API level columns.
* It uses the text field renderer ({@link #myRenderer}) and allows for sorting by the lexicographical value
* of the string displayed by the {@link com.intellij.ui.components.JBLabel} rendered as the cell component. An explicit width may be used
* by calling the overloaded constructor, otherwise the column will auto-scale to fill available space.
*/
public abstract class SystemImageColumnInfo extends ColumnInfo<SystemImageDescription, String> {
private final Border myBorder = IdeBorderFactory.createEmptyBorder(10, 10, 10, 10);
private final int myWidth;
public SystemImageColumnInfo(@NotNull String name, int width) {
super(name);
myWidth = width;
}
public SystemImageColumnInfo(@NotNull String name) {
this(name, -1);
}
@Override
public boolean isCellEditable(SystemImageDescription systemImageDescription) {
return systemImageDescription.isRemote();
}
@Nullable
@Override
public TableCellEditor getEditor(SystemImageDescription o) {
return new SystemImageDescriptionRenderer(o);
}
@Nullable
@Override
public TableCellRenderer getRenderer(final SystemImageDescription o) {
return new SystemImageDescriptionRenderer(o);
}
private class SystemImageDescriptionRenderer extends AbstractTableCellEditor implements TableCellRenderer {
private SystemImageDescription image;
SystemImageDescriptionRenderer(SystemImageDescription o) {
image = o;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
if (isSelected) {
if (image.isRemote()) {
panel.setBackground(UIUtil.getListUnfocusedSelectionBackground());
} else {
panel.setBackground(table.getSelectionBackground());
}
panel.setForeground(table.getSelectionForeground());
panel.setOpaque(true);
}
else {
panel.setBackground(table.getBackground());
panel.setForeground(table.getForeground());
panel.setOpaque(true);
}
JBLabel label = new JBLabel((String)value);
Font labelFont = UIUtil.getLabelFont();
if (column == 0) {
label.setFont(labelFont.deriveFont(Font.BOLD));
}
if (image.isRemote()) {
Font font = labelFont.deriveFont(label.getFont().getStyle() | Font.ITALIC);
label.setFont(font);
label.setForeground(UIUtil.getLabelDisabledForeground());
// on OS X the actual text width isn't computed correctly. Compensating for that..
if (!label.getText().isEmpty()) {
int fontMetricsWidth = label.getFontMetrics(label.getFont()).stringWidth(label.getText());
TextLayout l = new TextLayout(label.getText(), label.getFont(), label.getFontMetrics(label.getFont()).getFontRenderContext());
int offset = (int)Math.ceil(l.getBounds().getWidth()) - fontMetricsWidth;
if (offset > 0) {
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, offset));
}
}
panel.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER || e.getKeyChar() == KeyEvent.VK_SPACE) {
downloadImage(image);
}
}
});
}
panel.add(label);
if (image.isRemote() && column == 0) {
final JBLabel link = new JBLabel("Download");
link.setBackground(table.getBackground());
link.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
link.setForeground(JBColor.BLUE);
Font font = link.getFont();
if (isSelected) {
Map<TextAttribute, Integer> attrs = Maps.newHashMap();
attrs.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
font = font.deriveFont(attrs);
}
link.setFont(font);
link.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
downloadImage(image);
}
});
panel.add(link);
}
return panel;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
return getTableCellRendererComponent(table, value, isSelected, false, row, column);
}
@Override
public Object getCellEditorValue() {
return null;
}
@Override
public boolean isCellEditable(EventObject e) {
return true;
}
}
private void downloadImage(SystemImageDescription image) {
IPkgDesc request = image.getRemotePackage();
List<IPkgDesc> requestedPackages = Lists.newArrayList(request);
SdkQuickfixWizard sdkQuickfixWizard = new SdkQuickfixWizard(null, null, requestedPackages,
new DialogWrapperHost(null, DialogWrapper.IdeModalityType.PROJECT));
sdkQuickfixWizard.init();
sdkQuickfixWizard.show();
refreshImages(true);
}
@Nullable
@Override
public Comparator<SystemImageDescription> getComparator() {
return new Comparator<SystemImageDescription>() {
@Override
public int compare(SystemImageDescription o1, SystemImageDescription o2) {
String s1 = valueOf(o1);
String s2 = valueOf(o2);
return Comparing.compare(s1, s2);
}
};
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
}
}