blob: f7535265d9a7de4cd9e85516b7d0db1da163df3e [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.npw;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.MajorRevision;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.tools.idea.templates.TemplateMetadata;
import com.android.tools.idea.templates.TemplateUtils;
import com.android.tools.idea.ui.ComboBoxItemWithApiTag;
import com.android.tools.idea.wizard.dynamic.ScopedDataBinder;
import com.android.tools.idea.wizard.dynamic.ScopedStateStore;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
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 java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static com.android.tools.idea.npw.FormFactorUtils.*;
import static com.android.tools.idea.wizard.WizardConstants.INSTALL_REQUESTS_KEY;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Key;
/**
* A labeled combo box of SDK options for a given FormFactor.
*/
public final class FormFactorApiComboBox extends JComboBox {
private static final Logger LOG = Logger.getInstance(FormFactorApiComboBox.class);
// Set of installed targets and versions. TODO: These fields should not be static; that causes
// the versions to not stay up to date when new versions are installed. In the constructor
// we've removed the lazy evaluation, so for every new FormFactorApiComboBox that we construct,
// this (shared) list is reinitialized. Ideally we'd make these instance lists, but there's
// some control flow now where a wizard page updates these lists, so without making bigger
// changes to the code we'll leave it this way for now.
private static final Set<AndroidVersion> ourInstalledVersions = Sets.newHashSet();
private static final List<AndroidTargetComboBoxItem> ourTargets = Lists.newArrayList();
private static IAndroidTarget ourHighestInstalledApiTarget;
@NotNull private FormFactor myFormFactor;
private IPkgDesc myInstallRequest;
private Key<String> myBuildApiKey;
private Key<Integer> myBuildApiLevelKey;
private Key<Integer> myTargetApiLevelKey;
private Key<String> myTargetApiStringKey;
private Key<AndroidTargetComboBoxItem> myTargetComboBoxKey;
private Key<Boolean> myInclusionKey;
public FormFactorApiComboBox(@NotNull FormFactor formFactor, int minSdkLevel) {
init(formFactor, minSdkLevel);
}
public FormFactorApiComboBox() { }
public void init(@NotNull FormFactor formFactor, int minSdkLevel) {
myFormFactor = formFactor;
// These target lists used to be initialized just once. However, that resulted
// in a bug where after installing new targets, it keeps believing the new targets
// to not be available and requiring another install. We should just compute them
// once - it's not an expensive operation (calling both takes 1-2 ms.)
loadTargets();
loadInstalledVersions();
myBuildApiKey = FormFactorUtils.getBuildApiKey(formFactor);
myBuildApiLevelKey = FormFactorUtils.getBuildApiLevelKey(formFactor);
myTargetApiLevelKey = FormFactorUtils.getTargetApiLevelKey(formFactor);
myTargetApiStringKey = FormFactorUtils.getTargetApiStringKey(formFactor);
myTargetComboBoxKey = getTargetComboBoxKey(formFactor);
myInclusionKey = getInclusionKey(formFactor);
populateComboBox(formFactor, minSdkLevel);
loadSavedApi();
}
public void register(@NotNull ScopedDataBinder binder) {
assert myFormFactor != null : "register() called on FormFactorApiComboBox before init()";
binder.register(getTargetComboBoxKey(myFormFactor), this, TARGET_COMBO_BINDING);
}
/**
* Load the saved value for this ComboBox
*/
public void loadSavedApi() {
// Check for a saved value for the min api level
String savedApiLevel = PropertiesComponent.getInstance().getValue(FormFactorUtils.getPropertiesComponentMinSdkKey(myFormFactor),
Integer.toString(myFormFactor.defaultApi));
setSelectedItem(savedApiLevel);
// If the savedApiLevel is not available, just pick the first target in the list
// which is guaranteed to be a valid target because of the filtering done by populateComboBox()
if (getSelectedIndex() < 0 && getItemCount() > 0) {
setSelectedIndex(0);
}
}
/**
* Fill in the values that can be derived from the selected min SDK level:
*
* minApiLevel will be set to the selected api level (string or number)
* minApi will be set to the numerical equivalent
* buildApi will be set to the highest installed platform, or to the preview platform if a preview is selected
* buildApiString will be set to the corresponding string
* targetApi will be set to the highest installed platform or to the preview platform if a preview is selected
* targetApiString will be set to the corresponding string
* @param stateStore
* @param modified
*/
public void deriveValues(@NotNull ScopedStateStore stateStore, @NotNull Set<Key> modified) {
if (modified.contains(myTargetComboBoxKey) || modified.contains(myInclusionKey)) {
AndroidTargetComboBoxItem targetItem = stateStore.get(myTargetComboBoxKey);
if (targetItem == null) {
return;
}
stateStore.put(getMinApiKey(myFormFactor), targetItem.id.toString());
stateStore.put(getMinApiLevelKey(myFormFactor), targetItem.apiLevel);
IAndroidTarget target = targetItem.target;
if (target != null && (target.getVersion().isPreview() || !target.isPlatform())) {
// Make sure we set target and build to the preview version as well
populateApiLevels(targetItem.apiLevel, target, stateStore);
} else {
int targetApiLevel;
if (ourHighestInstalledApiTarget != null) {
targetApiLevel = ourHighestInstalledApiTarget.getVersion().getFeatureLevel();
} else {
targetApiLevel = 0;
}
populateApiLevels(targetApiLevel, ourHighestInstalledApiTarget, stateStore);
}
// Check to see if this is installed. If not, request that we install it
if (myInstallRequest != null) {
// First remove the last request, no need to install more than one platform
stateStore.listRemove(INSTALL_REQUESTS_KEY, myInstallRequest);
}
if (target == null) {
AndroidVersion androidVersion = new AndroidVersion(targetItem.apiLevel, null);
// TODO: If the user has no APIs installed, we then request to install whichever version the user has targeted here.
// Instead, we should choose to install the highest stable API possible. However, users having no SDK at all installed is pretty
// unlikely, so this logic can wait for a followup CL.
if (ourHighestInstalledApiTarget == null ||
(androidVersion.getApiLevel() > ourHighestInstalledApiTarget.getVersion().getApiLevel() &&
!ourInstalledVersions.contains(androidVersion) &&
stateStore.get(myInclusionKey))) {
IPkgDesc platformDescription =
PkgDesc.Builder.newPlatform(androidVersion, new MajorRevision(1), FullRevision.NOT_SPECIFIED).create();
stateStore.listPush(INSTALL_REQUESTS_KEY, platformDescription);
myInstallRequest = platformDescription;
populateApiLevels(androidVersion.getApiLevel(), ourHighestInstalledApiTarget, stateStore);
}
}
PropertiesComponent.getInstance().setValue(getPropertiesComponentMinSdkKey(myFormFactor), targetItem.id.toString());
// Check Java language level; should be 7 for L; eventually this will be automatically defaulted by the Android Gradle plugin
// instead: https://code.google.com/p/android/issues/detail?id=76252
String javaVersion = null;
if (ourHighestInstalledApiTarget != null && ourHighestInstalledApiTarget.getVersion().getFeatureLevel() >= 21) {
AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
if (sdkData != null) {
JavaSdk jdk = JavaSdk.getInstance();
Sdk sdk = ProjectJdkTable.getInstance().findMostRecentSdkOfType(jdk);
if (sdk != null) {
JavaSdkVersion version = jdk.getVersion(sdk);
if (version != null && version.isAtLeast(JavaSdkVersion.JDK_1_7)) {
javaVersion = JavaSdkVersion.JDK_1_7.getDescription();
}
}
}
}
stateStore.put(getLanguageLevelKey(myFormFactor), javaVersion);
}
}
public void setSelectedItem(String item) {
ScopedDataBinder.setSelectedItem(this, item);
}
private void populateComboBox(@NotNull FormFactorUtils.FormFactor formFactor, int minSdk) {
for (AndroidTargetComboBoxItem target :
Iterables.filter(ourTargets, FormFactorUtils.getMinSdkComboBoxFilter(formFactor, minSdk))) {
if (target.apiLevel >= minSdk || (target.target != null && target.target.getVersion().isPreview())) {
addItem(target);
}
}
}
/**
* Load the definitions of the android compilation targets
*/
private static void loadTargets() {
ourTargets.clear();
IAndroidTarget[] targets = getCompilationTargets();
if (AndroidSdkUtils.isAndroidSdkAvailable()) {
String[] knownVersions = TemplateUtils.getKnownVersions();
for (int i = 0; i < knownVersions.length; i++) {
AndroidTargetComboBoxItem targetInfo = new AndroidTargetComboBoxItem(knownVersions[i], i + 1);
ourTargets.add(targetInfo);
}
}
for (IAndroidTarget target : targets) {
if (target.getVersion().isPreview() || !target.getAdditionalLibraries().isEmpty()) {
AndroidTargetComboBoxItem targetInfo = new AndroidTargetComboBoxItem(target);
ourTargets.add(targetInfo);
}
}
}
/**
* Load the installed android versions from the SDK
*/
public static void loadInstalledVersions() {
IAndroidTarget[] targets = getCompilationTargets();
IAndroidTarget highestInstalledTarget = null;
ourInstalledVersions.clear();
for (IAndroidTarget target : targets) {
if (highestInstalledTarget == null ||
target.getVersion().getFeatureLevel() > highestInstalledTarget.getVersion().getFeatureLevel() &&
!target.getVersion().isPreview()) {
highestInstalledTarget = target;
}
if (target.getVersion().isPreview() || !target.getAdditionalLibraries().isEmpty()) {
AndroidTargetComboBoxItem targetInfo = new AndroidTargetComboBoxItem(target);
ourInstalledVersions.add(targetInfo.target.getVersion());
}
}
ourHighestInstalledApiTarget = highestInstalledTarget;
}
/**
* @return a list of android compilation targets (platforms and add-on SDKs)
*/
@NotNull
private static IAndroidTarget[] getCompilationTargets() {
AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
if (sdkData == null) {
return new IAndroidTarget[0];
}
return getCompilationTargets(sdkData);
}
@NotNull
public static IAndroidTarget[] getCompilationTargets(@NotNull AndroidSdkData sdkData) {
IAndroidTarget[] targets = sdkData.getTargets();
List<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
for (IAndroidTarget target : targets) {
if (!target.isPlatform() && target.getOptionalLibraries().isEmpty()) {
continue;
}
list.add(target);
}
return list.toArray(new IAndroidTarget[list.size()]);
}
public static class AndroidTargetComboBoxItem extends ComboBoxItemWithApiTag {
public int apiLevel = -1;
public IAndroidTarget target = null;
public AndroidTargetComboBoxItem(@NotNull String label, int apiLevel) {
super(Integer.toString(apiLevel), label, 1, 1);
this.apiLevel = apiLevel;
}
public AndroidTargetComboBoxItem(@NotNull IAndroidTarget target) {
super(getId(target), getLabel(target), 1, 1);
this.target = target;
apiLevel = target.getVersion().getFeatureLevel();
}
@NotNull
private static String getLabel(@NotNull IAndroidTarget target) {
if (target.isPlatform()
&& target.getVersion().getApiLevel() <= SdkVersionInfo.HIGHEST_KNOWN_API) {
if (target.getVersion().isPreview()) {
return target.getVersion().getApiString() + ": " + target.getName();
}
String name = SdkVersionInfo.getAndroidName(target.getVersion().getApiLevel());
if (name == null) {
return "API " + Integer.toString(target.getVersion().getApiLevel());
} else {
return name;
}
} else {
return AndroidSdkUtils.getTargetLabel(target);
}
}
@NotNull
private static String getId(@NotNull IAndroidTarget target) {
return target.getVersion().getApiString();
}
@Override
public String toString() {
return label;
}
}
static final ScopedDataBinder.ComponentBinding<AndroidTargetComboBoxItem, JComboBox> TARGET_COMBO_BINDING =
new ScopedDataBinder.ComponentBinding<AndroidTargetComboBoxItem, JComboBox>() {
@Override
public void setValue(@Nullable AndroidTargetComboBoxItem newValue, @NotNull JComboBox component) {
component.setSelectedItem(newValue);
}
@Nullable
@Override
public AndroidTargetComboBoxItem getValue(@NotNull JComboBox component) {
return (AndroidTargetComboBoxItem)component.getItemAt(component.getSelectedIndex());
}
@Override
public void addActionListener(@NotNull ActionListener listener, @NotNull JComboBox component) {
component.addActionListener(listener);
}
};
/**
* Populate the api variables in the given state store
* @param apiLevel the chosen build api level
* @param apiTarget the chosen target api level
* @param state the state in which the given variables will be set
*/
public void populateApiLevels(int apiLevel, @Nullable IAndroidTarget apiTarget, @NotNull ScopedStateStore state) {
if (apiLevel >= 1) {
if (apiTarget == null) {
state.put(myBuildApiKey, Integer.toString(apiLevel));
} else if (!apiTarget.isPlatform()) {
state.put(myBuildApiKey, AndroidTargetHash.getTargetHashString(apiTarget));
} else {
state.put(myBuildApiKey, TemplateMetadata.getBuildApiString(apiTarget.getVersion()));
}
state.put(myBuildApiLevelKey, apiLevel);
if (apiLevel >= SdkVersionInfo.HIGHEST_KNOWN_API || (apiTarget != null && apiTarget.getVersion().isPreview())) {
state.put(myTargetApiLevelKey, apiLevel);
if (apiTarget != null) {
state.put(myTargetApiStringKey, apiTarget.getVersion().getApiString());
} else {
state.put(myTargetApiStringKey, Integer.toString(apiLevel));
}
} else if (ourHighestInstalledApiTarget != null) {
state.put(myTargetApiLevelKey, ourHighestInstalledApiTarget.getVersion().getApiLevel());
state.put(myTargetApiStringKey, ourHighestInstalledApiTarget.getVersion().getApiString());
}
// Are we installing a new platform (so we don't have an IAndroidTarget yet) ?
// If so, adjust compile and target sdk to that new platform
if (apiTarget != null && apiLevel > apiTarget.getVersion().getApiLevel() && !apiTarget.getVersion().isPreview()) {
state.put(myBuildApiKey, Integer.toString(apiLevel));
state.put(myTargetApiStringKey, Integer.toString(apiLevel));
// myBuildApiLevelKey is already correct
}
}
}
}