/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.wizards.newproject; import com.android.SdkConstants; import com.android.annotations.Nullable; import com.android.ide.common.sdk.LoadStatus; import com.android.ide.common.xml.AndroidManifestParser; import com.android.ide.common.xml.ManifestData; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; import com.android.io.FileWrapper; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkManager; import com.android.sdkuilib.internal.widgets.SdkTargetSelector; import com.android.utils.NullLogger; import com.android.utils.Pair; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Group; import java.io.File; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; /** A page in the New Project wizard where you select the target SDK */ class SdkSelectionPage extends WizardPage implements ITargetChangeListener { private final NewProjectWizardState mValues; private boolean mIgnore; private SdkTargetSelector mSdkTargetSelector; /** * Create the wizard. */ SdkSelectionPage(NewProjectWizardState values) { super("sdkSelection"); //$NON-NLS-1$ mValues = values; setTitle("Select Build Target"); AdtPlugin.getDefault().addTargetListener(this); } @Override public void dispose() { AdtPlugin.getDefault().removeTargetListener(this); super.dispose(); } /** * Create contents of the wizard. */ @Override public void createControl(Composite parent) { Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); // Layout has 1 column group.setLayout(new GridLayout()); group.setLayoutData(new GridData(GridData.FILL_BOTH)); group.setFont(parent.getFont()); group.setText("Build Target"); // The selector is created without targets. They are added below in the change listener. mSdkTargetSelector = new SdkTargetSelector(group, null); mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mIgnore) { return; } mValues.target = mSdkTargetSelector.getSelected(); mValues.targetModifiedByUser = true; onSdkTargetModified(); validatePage(); } }); onSdkLoaded(); setControl(group); } @Override public void setVisible(boolean visible) { super.setVisible(visible); if (mValues.mode == Mode.SAMPLE) { setDescription("Choose an SDK to select a sample from"); } else { setDescription("Choose an SDK to target"); } try { mIgnore = true; if (mValues.target != null) { mSdkTargetSelector.setSelection(mValues.target); } } finally { mIgnore = false; } validatePage(); } @Override public boolean isPageComplete() { // Ensure that the Finish button isn't enabled until // the user has reached and completed this page if (mValues.target == null) { return false; } return super.isPageComplete(); } /** * Called when an SDK target is modified. * * Also changes the minSdkVersion field to reflect the sdk api level that has * just been selected. */ private void onSdkTargetModified() { if (mIgnore) { return; } IAndroidTarget target = mValues.target; // Update the minimum SDK text field? // We do if one of two conditions are met: if (target != null) { boolean setMinSdk = false; AndroidVersion version = target.getVersion(); int apiLevel = version.getApiLevel(); // 1. Has the user not manually edited the SDK field yet? If so, keep // updating it to the selected value. if (!mValues.minSdkModifiedByUser) { setMinSdk = true; } else { // 2. Is the API level set to a higher level than the newly selected // target SDK? If so, change it down to the new lower value. String s = mValues.minSdk; if (s.length() > 0) { try { int currentApi = Integer.parseInt(s); if (currentApi > apiLevel) { setMinSdk = true; } } catch (NumberFormatException nfe) { // User may have typed something invalid -- ignore } } } if (setMinSdk) { String minSdk; if (version.isPreview()) { minSdk = version.getCodename(); } else { minSdk = Integer.toString(apiLevel); } mValues.minSdk = minSdk; } } loadSamplesForTarget(target); } /** * Updates the list of all samples for the given target SDK. * The list is stored in mSamplesPaths as absolute directory paths. * The combo is recreated to match this. */ private void loadSamplesForTarget(IAndroidTarget target) { // Keep the name of the old selection (if there were any samples) File previouslyChosenSample = mValues.chosenSample; mValues.samples.clear(); mValues.chosenSample = null; if (target != null) { // Get the sample root path and recompute the list of samples String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); File root = new File(samplesRootPath); findSamplesManifests(root, root, null, null, mValues.samples); Sdk sdk = Sdk.getCurrent(); if (sdk != null) { // Parse the extras to see if we can find samples that are // compatible with the selected target API. // First we need an SdkManager that suppresses all output. SdkManager sdkman = sdk.getNewSdkManager(NullLogger.getLogger()); Map<File, String> extras = sdkman.getExtraSamples(); for (Entry<File, String> entry : extras.entrySet()) { File path = entry.getKey(); String name = entry.getValue(); // Case where the sample is at the root of the directory and not // in a per-sample sub-directory. if (path.getName().equals(SdkConstants.FD_SAMPLE)) { findSampleManifestInDir( path, path, name, target.getVersion(), mValues.samples); } // Scan sub-directories findSamplesManifests( path, path, name, target.getVersion(), mValues.samples); } } if (mValues.samples.isEmpty()) { return; } else { Collections.sort(mValues.samples, new Comparator<Pair<String, File>>() { @Override public int compare(Pair<String, File> o1, Pair<String, File> o2) { // Compare the display name of the sample return o1.getFirst().compareTo(o2.getFirst()); } }); } // Try to find the old selection. if (previouslyChosenSample != null) { String previouslyChosenName = previouslyChosenSample.getName(); for (int i = 0, n = mValues.samples.size(); i < n; i++) { File file = mValues.samples.get(i).getSecond(); if (file.getName().equals(previouslyChosenName)) { mValues.chosenSample = file; break; } } } } } /** * Recursively find potential sample directories under the given directory. * Actually lists any directory that contains an android manifest. * Paths found are added the samplesPaths list. * * @param rootDir The "samples" root directory. Doesn't change during recursion. * @param currDir The directory being scanned. Caller must initially set it to {@code rootDir}. * @param extraName Optional name appended to the samples display name. Typically used to * indicate a sample comes from a given extra package. * @param targetVersion Optional target version filter. If non null, only samples that are * compatible with the given target will be listed. * @param samplesPaths A non-null list filled by this method with all samples found. The * pair is (String: sample display name => File: sample directory). */ private void findSamplesManifests( File rootDir, File currDir, @Nullable String extraName, @Nullable AndroidVersion targetVersion, List<Pair<String, File>> samplesPaths) { if (!currDir.isDirectory()) { return; } for (File f : currDir.listFiles()) { if (f.isDirectory()) { findSampleManifestInDir(f, rootDir, extraName, targetVersion, samplesPaths); // Recurse in the project, to find embedded tests sub-projects // We can however skip this recursion for known android sub-dirs that // can't have projects, namely for sources, assets and resources. String leaf = f.getName(); if (!SdkConstants.FD_SOURCES.equals(leaf) && !SdkConstants.FD_ASSETS.equals(leaf) && !SdkConstants.FD_RES.equals(leaf)) { findSamplesManifests(rootDir, f, extraName, targetVersion, samplesPaths); } } } } private void findSampleManifestInDir( File sampleDir, File rootDir, String extraName, AndroidVersion targetVersion, List<Pair<String, File>> samplesPaths) { // Assume this is a sample if it contains an android manifest. File manifestFile = new File(sampleDir, SdkConstants.FN_ANDROID_MANIFEST_XML); if (manifestFile.isFile()) { try { ManifestData data = AndroidManifestParser.parse(new FileWrapper(manifestFile)); if (data != null) { boolean accept = false; if (targetVersion == null) { accept = true; } else if (targetVersion != null) { int i = data.getMinSdkVersion(); if (i != ManifestData.MIN_SDK_CODENAME) { accept = i <= targetVersion.getApiLevel(); } else { String s = data.getMinSdkVersionString(); if (s != null) { accept = s.equals(targetVersion.getCodename()); } } } if (accept) { String name = getSampleDisplayName(extraName, rootDir, sampleDir); samplesPaths.add(Pair.of(name, sampleDir)); } } } catch (Exception e) { // Ignore. Don't use a sample which manifest doesn't parse correctly. AdtPlugin.log(IStatus.INFO, "NPW ignoring malformed manifest %s", //$NON-NLS-1$ manifestFile.getAbsolutePath()); } } } /** * Compute the sample name compared to its root directory. */ private String getSampleDisplayName(String extraName, File rootDir, File sampleDir) { String name = null; if (!rootDir.equals(sampleDir)) { String path = sampleDir.getPath(); int n = rootDir.getPath().length(); if (path.length() > n) { path = path.substring(n); if (path.charAt(0) == File.separatorChar) { path = path.substring(1); } if (path.endsWith(File.separator)) { path = path.substring(0, path.length() - 1); } name = path.replaceAll(Pattern.quote(File.separator), " > "); //$NON-NLS-1$ } } if (name == null && rootDir.equals(sampleDir) && sampleDir.getName().equals(SdkConstants.FD_SAMPLE) && extraName != null) { // This is an old-style extra with one single sample directory. Just use the // extra's name as the same name. return extraName; } if (name == null) { // Otherwise try to use the sample's directory name as the sample name. while (sampleDir != null && (name == null || SdkConstants.FD_SAMPLE.equals(name) || SdkConstants.FD_SAMPLES.equals(name))) { name = sampleDir.getName(); sampleDir = sampleDir.getParentFile(); } } if (name == null) { if (extraName != null) { // In the unlikely case nothing worked and we have an extra name, use that. return extraName; } else { name = "Sample"; // fallback name... should not happen. //$NON-NLS-1$ } } if (extraName != null) { name = name + " [" + extraName + ']'; //$NON-NLS-1$ } return name; } private void validatePage() { String error = null; if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADING) { error = "The SDK is still loading; please wait."; } if (error == null && mValues.target == null) { error = "An SDK Target must be specified."; } if (error == null && mValues.mode == Mode.SAMPLE) { // Make sure this SDK target contains samples if (mValues.samples == null || mValues.samples.size() == 0) { error = "This target has no samples. Please select another target."; } } // -- update UI & enable finish if there's no error setPageComplete(error == null); if (error != null) { setMessage(error, IMessageProvider.ERROR); } else { setErrorMessage(null); setMessage(null); } } // ---- Implements ITargetChangeListener ---- @Override public void onSdkLoaded() { if (mSdkTargetSelector == null) { return; } // Update the sdk target selector with the new targets // get the targets from the sdk IAndroidTarget[] targets = null; if (Sdk.getCurrent() != null) { targets = Sdk.getCurrent().getTargets(); } mSdkTargetSelector.setTargets(targets); // If there's only one target, select it. // This will invoke the selection listener on the selector defined above. if (targets != null && targets.length == 1) { mValues.target = targets[0]; mSdkTargetSelector.setSelection(mValues.target); onSdkTargetModified(); } else if (targets != null) { // Pick the highest available platform by default (see issue #17505 // for related discussion.) IAndroidTarget initialTarget = null; for (IAndroidTarget target : targets) { if (target.isPlatform() && !target.getVersion().isPreview() && (initialTarget == null || target.getVersion().getApiLevel() > initialTarget.getVersion().getApiLevel())) { initialTarget = target; } } if (initialTarget != null) { mValues.target = initialTarget; try { mIgnore = true; mSdkTargetSelector.setSelection(mValues.target); } finally { mIgnore = false; } onSdkTargetModified(); } } validatePage(); } @Override public void onProjectTargetChange(IProject changedProject) { // Ignore } @Override public void onTargetLoaded(IAndroidTarget target) { // Ignore } }