blob: 6cafcf0575a449eb6dd1903612b03cb0dbbac6ea [file] [log] [blame]
/*
* 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
}
}