blob: e4638d017b294847af1d08d61593c34e48a1e460 [file] [log] [blame]
/*
* Copyright (C) 2013 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.wizard;
import com.android.sdklib.repository.descriptors.IPkgDesc;
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.stats.DistributionService;
import com.android.tools.idea.templates.Template;
import com.android.tools.idea.templates.TemplateManager;
import com.android.tools.idea.templates.TemplateMetadata;
import com.google.common.collect.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.ui.HyperlinkAdapter;
import com.intellij.ui.HyperlinkLabel;
import com.intellij.ui.JBCardLayout;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.util.ui.AsyncProcessIcon;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.View;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import static com.android.tools.idea.templates.TemplateMetadata.*;
import static com.android.tools.idea.wizard.FormFactorApiComboBox.AndroidTargetComboBoxItem;
import static com.android.tools.idea.wizard.FormFactorUtils.*;
import static com.android.tools.idea.wizard.FormFactorUtils.FormFactor.MOBILE;
import static com.android.tools.idea.wizard.ScopedStateStore.Key;
import static com.android.tools.idea.wizard.ScopedStateStore.Scope.STEP;
import static com.android.tools.idea.wizard.ScopedStateStore.Scope.WIZARD;
import static com.android.tools.idea.wizard.ScopedStateStore.createKey;
import static com.android.tools.idea.wizard.WizardConstants.NEWLY_INSTALLED_API_KEY;
/**
* ConfigureAndroidModuleStep is the first page in the New Project wizard that sets project/module name, location, and other project-global
* parameters.
*/
public class ConfigureFormFactorStep extends DynamicWizardStepWithHeaderAndDescription {
public static final String MIN_SDK_STRING = "Minimum SDK";
private static final Key<String> API_FEEDBACK_KEY = createKey("API Feedback", STEP, String.class);
public static final Key<Integer> NUM_ENABLED_FORM_FACTORS_KEY = createKey("NumberOfEnabledFormFactors", WIZARD, Integer.class);
private JPanel myPanel;
private JPanel myFormFactorPanel;
private JBLabel myHelpMeChooseLabel = new JBLabel(getApiHelpText(0)) {
@Override
public Dimension getPreferredSize() {
// Since this contains auto-wrapped text, the preferred height will not be set until repaint(). The below will set it as soon
// as the actual width is known. This allows the wizard dialog to be set to the correct size even before this step is shown.
final View view = (View)myHelpMeChooseLabel.getClientProperty("html");
if (getWidth() > 0) {
view.setSize(getWidth(), 0);
setPreferredSize(new Dimension((int)view.getPreferredSpan(View.X_AXIS), (int)view.getPreferredSpan(View.Y_AXIS)));
}
return super.getPreferredSize();
}
};
private HyperlinkLabel myHelpMeChooseLink = new HyperlinkLabel("Help me choose");
private List<Pair<Key<Boolean>, JCheckBox>> myCheckboxKeys = Lists.newArrayList();
private List<FormFactor> myFormFactors = Lists.newArrayList();
private ChooseApiLevelDialog myChooseApiLevelDialog = new ChooseApiLevelDialog(null, -1);
private Disposable myDisposable;
private Map<FormFactor, FormFactorSdkControls> myFormFactorApiSelectors = Maps.newHashMap();
private static final String DOWNLOAD_LINK_CARD = "link";
private static final String DOWNLOAD_PROGRESS_CARD = "progress";
public ConfigureFormFactorStep(@NotNull Disposable disposable) {
super("Select the form factors your app will run on", "Different platforms require separate SDKs", disposable);
myDisposable = disposable;
Disposer.register(disposable, myChooseApiLevelDialog.getDisposable());
setBodyComponent(myPanel);
populateAdditionalFormFactors();
}
@Override
public void init() {
super.init();
myHelpMeChooseLink.addHyperlinkListener(new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
Integer minApiLevel = myState.get(getMinApiLevelKey(MOBILE));
myChooseApiLevelDialog = new ChooseApiLevelDialog(null, minApiLevel == null ? 0 : minApiLevel);
Disposer.register(myDisposable, myChooseApiLevelDialog.getDisposable());
if (myChooseApiLevelDialog.showAndGet()) {
int selectedApiLevel = myChooseApiLevelDialog.getSelectedApiLevel();
myFormFactorApiSelectors.get(MOBILE).setSelectedItem(Integer.toString(selectedApiLevel));
}
}
});
register(API_FEEDBACK_KEY, myHelpMeChooseLabel, new ComponentBinding<String, JBLabel>() {
@Override
public void setValue(@Nullable String newValue, @NotNull JBLabel label) {
final JBLabel referenceLabel = label;
final String referenceString = newValue;
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
referenceLabel.setText(referenceString);
}
});
}
});
registerValueDeriver(API_FEEDBACK_KEY, new ValueDeriver<String>() {
@Nullable
@Override
public Set<Key<?>> getTriggerKeys() {
return makeSetOf(getTargetComboBoxKey(MOBILE));
}
@Nullable
@Override
public String deriveValue(@NotNull ScopedStateStore state, Key changedKey, @Nullable String currentValue) {
AndroidTargetComboBoxItem selectedItem = state.get(getTargetComboBoxKey(MOBILE));
return getApiHelpText(selectedItem == null ? 0 : selectedItem.apiLevel);
}
});
Set<Key> keys = Sets.newHashSet();
for (Pair<Key<Boolean>, JCheckBox> item : myCheckboxKeys) {
keys.add(item.getFirst());
register(item.getFirst(), item.getSecond());
}
myState.put(myCheckboxKeys.get(0).getFirst(), true);
// Since the controls are created before the state is completely set up (before the step is
// attached to the wizard) we have to explicitly load the saved state here.
for (FormFactorSdkControls control : myFormFactorApiSelectors.values()) {
control.getMinSdkCombo().loadSavedApi();
}
}
private static String getApiHelpText(int selectedApi) {
float percentage = (float)(DistributionService.getInstance().getSupportedDistributionForApiLevel(selectedApi) * 100);
return String.format(Locale.getDefault(), "<html>Lower API levels target more devices, but have fewer features available. " +
"By targeting API %1$d and later, your app will run on %2$s of the devices that are " +
"active on the Google Play Store.</html>",
selectedApi,
percentage < 1 ? "&lt; 1%" : String.format(Locale.getDefault(), "approximately <b>%.1f%%</b>", percentage));
}
private void populateAdditionalFormFactors() {
TemplateManager manager = TemplateManager.getInstance();
List<File> applicationTemplates = manager.getTemplatesInCategory(Template.CATEGORY_APPLICATION);
myFormFactors.clear();
myFormFactorPanel.removeAll();
int row = 0;
final Map<FormFactor, Integer> minSdks = Maps.newHashMap();
for (File templateFile : applicationTemplates) {
TemplateMetadata metadata = manager.getTemplate(templateFile);
if (metadata == null || metadata.getFormFactor() == null) {
continue;
}
FormFactor formFactor = FormFactor.get(metadata.getFormFactor());
if (formFactor == null) {
continue;
}
myFormFactors.add(formFactor);
minSdks.put(formFactor, metadata.getMinSdk());
}
// Number of rows is two for each active form factor (one for checkbox and one for popup) + one for each of the
// message about % of devices covered and the "help me choose" link.
GridLayoutManager gridLayoutManager = new GridLayoutManager(myFormFactors.size() * 2 + 2, 2);
gridLayoutManager.setVGap(5);
gridLayoutManager.setHGap(10);
myFormFactorPanel.setLayout(gridLayoutManager);
for (final FormFactor formFactor : myFormFactors) {
GridConstraints c = new GridConstraints();
c.setRow(row);
c.setColumn(0);
c.setFill(GridConstraints.FILL_NONE);
c.setAnchor(GridConstraints.ANCHOR_WEST);
JCheckBox inclusionCheckBox = new JCheckBox(formFactor.toString());
myFormFactorPanel.add(inclusionCheckBox, c);
// Since we aren't connected to the wizard yet, we can't save this (wizard-scoped) state.
// Save away the key and component so they can be registered later.
myCheckboxKeys.add(Pair.create(FormFactorUtils.getInclusionKey(formFactor), inclusionCheckBox));
FormFactorSdkControls controls = new FormFactorSdkControls(formFactor, minSdks.get(formFactor));
FormFactorApiComboBox minSdkComboBox = controls.getMinSdkCombo();
minSdkComboBox.setName(formFactor.id + ".minSdk");
controls.layout(myFormFactorPanel, ++row, inclusionCheckBox.getIconTextGap());
myFormFactorApiSelectors.put(formFactor, controls);
minSdkComboBox.register(this);
// If we don't have any valid targets for the given form factor, disable that form factor
if (minSdkComboBox.getItemCount() == 0) {
inclusionCheckBox.setSelected(false);
inclusionCheckBox.setEnabled(false);
inclusionCheckBox.setText(inclusionCheckBox.getText() + " (Not Installed)");
JBCardLayout layout = new JBCardLayout();
final JPanel downloadCardPanel = new JPanel(layout);
downloadCardPanel.add(DOWNLOAD_PROGRESS_CARD, createDownloadingMessage());
final HyperlinkLabel link = new HyperlinkLabel("Download");
downloadCardPanel.add(DOWNLOAD_LINK_CARD, link);
layout.show(downloadCardPanel, DOWNLOAD_PROGRESS_CARD);
c.setColumn(1);
myFormFactorPanel.add(downloadCardPanel, c);
findCompatibleSdk(formFactor, minSdks.get(formFactor), link, downloadCardPanel);
}
if (formFactor.equals(MOBILE)) {
c.setRow(++row);
c.setColumn(1);
c.setAnchor(GridConstraints.ANCHOR_NORTHWEST);
c.setFill(GridConstraints.FILL_HORIZONTAL);
myFormFactorPanel.add(myHelpMeChooseLabel, c);
c.setRow(++row);
myFormFactorPanel.add(myHelpMeChooseLink, c);
}
row++;
}
}
private static JComponent createDownloadingMessage() {
JPanel downloadPanel = new JPanel(new FlowLayout());
AsyncProcessIcon refreshIcon = new AsyncProcessIcon("loading");
JLabel refreshingLabel = new JLabel("Looking for SDK...");
refreshingLabel.setForeground(JBColor.GRAY);
downloadPanel.add(refreshIcon);
downloadPanel.add(refreshingLabel);
return downloadPanel;
}
private void findCompatibleSdk(final FormFactor formFactor, final int minSdkLevel, final HyperlinkLabel link, final JPanel cardPanel) {
final SdkState state = SdkState.getInstance(AndroidSdkUtils.tryToChooseAndroidSdk());
Runnable onComplete = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
List<RemotePkgInfo> packageList = Lists.newArrayList(state.getUpdates().getNewPkgs());
Collections.sort(packageList);
Iterator<RemotePkgInfo> result =
Iterables.filter(packageList, FormFactorUtils.getMinSdkPackageFilter(formFactor, minSdkLevel)).iterator();
if (result.hasNext()) {
showDownloadLink(link, result.next(), cardPanel);
}
else {
cardPanel.setVisible(false);
}
}
});
}
};
Runnable onError = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
cardPanel.setVisible(false);
}
});
}
};
state.loadAsync(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, false, null, onComplete, onError, false);
}
private void showDownloadLink(final HyperlinkLabel link, final RemotePkgInfo remote, final JPanel cardPanel) {
link.addHyperlinkListener(new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
showDownloadWizard(remote.getPkgDesc());
populateAdditionalFormFactors();
myFormFactorPanel.validate();
}
});
((JBCardLayout)cardPanel.getLayout()).show(cardPanel, DOWNLOAD_LINK_CARD);
}
private static void showDownloadWizard(IPkgDesc pack) {
List<IPkgDesc> requestedPackages = Lists.newArrayList(pack);
SdkQuickfixWizard sdkQuickfixWizard = new SdkQuickfixWizard(null, null, requestedPackages,
new DialogWrapperHost(null, DialogWrapper.IdeModalityType.PROJECT));
sdkQuickfixWizard.init();
sdkQuickfixWizard.show();
}
@Override
public void onEnterStep() {
super.onEnterStep();
if (myState.containsKey(NEWLY_INSTALLED_API_KEY)) {
FormFactorApiComboBox.loadInstalledVersions();
}
}
@Override
public void deriveValues(Set<Key> modified) {
super.deriveValues(modified);
// Persist the min API level choices on a per-form factor basis
int enabledFormFactors = 0;
for (FormFactor formFactor : myFormFactors) {
Boolean included = myState.get(getInclusionKey(formFactor));
// Disable api selection for non-enabled form factors and check to see if only one is selected
if (included != null) {
if (myFormFactorApiSelectors.containsKey(formFactor)) {
myFormFactorApiSelectors.get(formFactor).setEnabled(included);
}
if (included) {
enabledFormFactors++;
FormFactorSdkControls controls = myFormFactorApiSelectors.get(formFactor);
if (controls != null) {
controls.getMinSdkCombo().deriveValues(myState, modified);
}
}
}
}
myState.put(NUM_ENABLED_FORM_FACTORS_KEY, enabledFormFactors);
}
@Override
public boolean validate() {
setErrorHtml("");
Integer enabledFormFactors = myState.get(NUM_ENABLED_FORM_FACTORS_KEY);
if (enabledFormFactors == null || enabledFormFactors < 1) {
// Don't allow an empty project
setErrorHtml("At least one form factor must be selected.");
return false;
}
for (FormFactor formFactor : myFormFactors) {
Boolean included = myState.get(getInclusionKey(formFactor));
// Disable api selection for non-enabled form factors and check to see if only one is selected
if (included != null && included) {
if (myState.get(getMinApiKey(formFactor)) == null) {
// Don't allow the user to continue unless all minAPIs are chosen
setErrorHtml("Each form factor must have a Minimum SDK level selected.");
return false;
}
}
}
return true;
}
@NotNull
@Override
public String getStepName() {
return "Configure Form Factors";
}
@Nullable
public String getHelpText(@NotNull String param) {
if (param.equals(ATTR_MIN_API)) {
return "Choose the lowest version of Android that your application will support. Lower API levels target more devices, " +
"but means fewer features are available. By targeting API 10 and later, you reach approximately 99% of the market.";
} else if (param.equals(ATTR_TARGET_API)) {
return "Choose the highest API level that the application is known to work with. This attribute informs the system that you have " +
"tested against the target version and the system should not enable any compatibility behaviors to maintain your app's " +
"forward-compatibility with the target version. The application is still able to run on older versions (down to " +
"minSdkVersion). Your application may look dated if you are not targeting the current version.";
} else if (param.equals(ATTR_BUILD_API)) {
return "Choose a target API to compile your code against, from your installed SDKs. This is typically the most recent version, " +
"or the first version that supports all the APIs you want to directly access without reflection.";
} else {
return null;
}
}
@Override
public JComponent getPreferredFocusedComponent() {
return myPanel;
}
@NotNull
@Override
protected WizardStepHeaderSettings getStepHeader() {
return WizardStepHeaderSettings.createTitleOnlyHeader("Target Android Devices");
}
private static final class FormFactorSdkControls {
private final JLabel myLabel;
private final FormFactorApiComboBox myMinSdkCombobox;
public FormFactorSdkControls(FormFactor formFactor, int minApi) {
myLabel = new JLabel(MIN_SDK_STRING);
myMinSdkCombobox = new FormFactorApiComboBox(formFactor, minApi);
}
public void setEnabled(boolean enabled) {
for (JComponent component : ImmutableSet.of(myLabel, myMinSdkCombobox)) {
component.setEnabled(enabled);
}
}
public void setSelectedItem(String item) {
ScopedDataBinder.setSelectedItem(myMinSdkCombobox, item);
}
public JLabel getLabel() {
return myLabel;
}
public FormFactorApiComboBox getMinSdkCombo() {
return myMinSdkCombobox;
}
private void layout(JPanel panel, int row, int ident) {
GridConstraints c = new GridConstraints();
c.setVSizePolicy(GridConstraints.SIZEPOLICY_FIXED);
c.setRow(row);
c.setColumn(0);
c.setFill(GridConstraints.FILL_NONE);
c.setAnchor(GridConstraints.ANCHOR_WEST);
c.setRow(row);
c.setIndent(ident);
panel.add(myLabel, c);
c.setIndent(0);
c.setColumn(1);
c.setFill(GridConstraints.FILL_HORIZONTAL);
panel.add(myMinSdkCombobox, c);
}
}
}