/* * 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.wizard; 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.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.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.wizard.FormFactorUtils.*; import static com.android.tools.idea.wizard.ScopedStateStore.Key; import static com.android.tools.idea.wizard.WizardConstants.INSTALL_REQUESTS_KEY; /** * A labeled combo box of SDK options for a given FormFactor. */ public final class FormFactorApiComboBox extends JComboBox { private static final Set<AndroidVersion> ourInstalledVersions = Sets.newHashSet(); private static final List<AndroidTargetComboBoxItem> ourTargets = Lists.newArrayList(); private static IAndroidTarget ourHighestInstalledApiTarget; @NotNull private final FormFactor myFormFactor; private IPkgDesc myInstallRequest; private final Key<String> myBuildApiKey; private final Key<Integer> myBuildApiLevelKey; private final Key<Integer> myTargetApiLevelKey; private final Key<String> myTargetApiStringKey; private final Key<AndroidTargetComboBoxItem> myTargetComboBoxKey; private final Key<Boolean> myInclusionKey; public FormFactorApiComboBox(@NotNull FormFactor formFactor, int minSdkLevel) { myFormFactor = formFactor; if (ourHighestInstalledApiTarget == null) { // Do one time initialization here 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) { 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 (target != null && target.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() { 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.getOptionalLibraries() != null && target.getOptionalLibraries().length > 0) { 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; for (IAndroidTarget target : targets) { if (highestInstalledTarget == null || target.getVersion().getFeatureLevel() > highestInstalledTarget.getVersion().getFeatureLevel() && !target.getVersion().isPreview()) { highestInstalledTarget = target; } if (target.getVersion().isPreview() || target.getOptionalLibraries() != null && target.getOptionalLibraries().length > 0) { 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() == null || target.getOptionalLibraries().length == 0)) { continue; } list.add(target); } return list.toArray(new IAndroidTarget[list.size()]); } public static class AndroidTargetComboBoxItem extends ComboBoxItem { 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 "API " + Integer.toString(target.getVersion().getApiLevel()) + "+: " + 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.getOptionalLibraries() != null) { 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()); } } } }