1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.ide.eclipse.adt.internal.wizards.newproject; 17 18import com.android.SdkConstants; 19import com.android.annotations.Nullable; 20import com.android.ide.common.sdk.LoadStatus; 21import com.android.ide.common.xml.AndroidManifestParser; 22import com.android.ide.common.xml.ManifestData; 23import com.android.ide.eclipse.adt.AdtPlugin; 24import com.android.ide.eclipse.adt.internal.sdk.Sdk; 25import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 26import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; 27import com.android.io.FileWrapper; 28import com.android.sdklib.AndroidVersion; 29import com.android.sdklib.IAndroidTarget; 30import com.android.sdklib.SdkManager; 31import com.android.sdkuilib.internal.widgets.SdkTargetSelector; 32import com.android.utils.NullLogger; 33import com.android.utils.Pair; 34 35import org.eclipse.core.resources.IProject; 36import org.eclipse.core.runtime.IStatus; 37import org.eclipse.jface.dialogs.IMessageProvider; 38import org.eclipse.jface.wizard.WizardPage; 39import org.eclipse.swt.SWT; 40import org.eclipse.swt.events.SelectionAdapter; 41import org.eclipse.swt.events.SelectionEvent; 42import org.eclipse.swt.layout.GridData; 43import org.eclipse.swt.layout.GridLayout; 44import org.eclipse.swt.widgets.Composite; 45import org.eclipse.swt.widgets.Group; 46 47import java.io.File; 48import java.util.Collections; 49import java.util.Comparator; 50import java.util.List; 51import java.util.Map; 52import java.util.Map.Entry; 53import java.util.regex.Pattern; 54 55/** A page in the New Project wizard where you select the target SDK */ 56class SdkSelectionPage extends WizardPage implements ITargetChangeListener { 57 private final NewProjectWizardState mValues; 58 private boolean mIgnore; 59 private SdkTargetSelector mSdkTargetSelector; 60 61 /** 62 * Create the wizard. 63 */ 64 SdkSelectionPage(NewProjectWizardState values) { 65 super("sdkSelection"); //$NON-NLS-1$ 66 mValues = values; 67 68 setTitle("Select Build Target"); 69 AdtPlugin.getDefault().addTargetListener(this); 70 } 71 72 @Override 73 public void dispose() { 74 AdtPlugin.getDefault().removeTargetListener(this); 75 super.dispose(); 76 } 77 78 /** 79 * Create contents of the wizard. 80 */ 81 @Override 82 public void createControl(Composite parent) { 83 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); 84 // Layout has 1 column 85 group.setLayout(new GridLayout()); 86 group.setLayoutData(new GridData(GridData.FILL_BOTH)); 87 group.setFont(parent.getFont()); 88 group.setText("Build Target"); 89 90 // The selector is created without targets. They are added below in the change listener. 91 mSdkTargetSelector = new SdkTargetSelector(group, null); 92 93 mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { 94 @Override 95 public void widgetSelected(SelectionEvent e) { 96 if (mIgnore) { 97 return; 98 } 99 100 mValues.target = mSdkTargetSelector.getSelected(); 101 mValues.targetModifiedByUser = true; 102 onSdkTargetModified(); 103 validatePage(); 104 } 105 }); 106 107 onSdkLoaded(); 108 109 setControl(group); 110 } 111 112 @Override 113 public void setVisible(boolean visible) { 114 super.setVisible(visible); 115 if (mValues.mode == Mode.SAMPLE) { 116 setDescription("Choose an SDK to select a sample from"); 117 } else { 118 setDescription("Choose an SDK to target"); 119 } 120 try { 121 mIgnore = true; 122 if (mValues.target != null) { 123 mSdkTargetSelector.setSelection(mValues.target); 124 } 125 } finally { 126 mIgnore = false; 127 } 128 129 validatePage(); 130 } 131 132 @Override 133 public boolean isPageComplete() { 134 // Ensure that the Finish button isn't enabled until 135 // the user has reached and completed this page 136 if (mValues.target == null) { 137 return false; 138 } 139 140 return super.isPageComplete(); 141 } 142 143 /** 144 * Called when an SDK target is modified. 145 * 146 * Also changes the minSdkVersion field to reflect the sdk api level that has 147 * just been selected. 148 */ 149 private void onSdkTargetModified() { 150 if (mIgnore) { 151 return; 152 } 153 154 IAndroidTarget target = mValues.target; 155 156 // Update the minimum SDK text field? 157 // We do if one of two conditions are met: 158 if (target != null) { 159 boolean setMinSdk = false; 160 AndroidVersion version = target.getVersion(); 161 int apiLevel = version.getApiLevel(); 162 // 1. Has the user not manually edited the SDK field yet? If so, keep 163 // updating it to the selected value. 164 if (!mValues.minSdkModifiedByUser) { 165 setMinSdk = true; 166 } else { 167 // 2. Is the API level set to a higher level than the newly selected 168 // target SDK? If so, change it down to the new lower value. 169 String s = mValues.minSdk; 170 if (s.length() > 0) { 171 try { 172 int currentApi = Integer.parseInt(s); 173 if (currentApi > apiLevel) { 174 setMinSdk = true; 175 } 176 } catch (NumberFormatException nfe) { 177 // User may have typed something invalid -- ignore 178 } 179 } 180 } 181 if (setMinSdk) { 182 String minSdk; 183 if (version.isPreview()) { 184 minSdk = version.getCodename(); 185 } else { 186 minSdk = Integer.toString(apiLevel); 187 } 188 mValues.minSdk = minSdk; 189 } 190 } 191 192 loadSamplesForTarget(target); 193 } 194 195 /** 196 * Updates the list of all samples for the given target SDK. 197 * The list is stored in mSamplesPaths as absolute directory paths. 198 * The combo is recreated to match this. 199 */ 200 private void loadSamplesForTarget(IAndroidTarget target) { 201 // Keep the name of the old selection (if there were any samples) 202 File previouslyChosenSample = mValues.chosenSample; 203 204 mValues.samples.clear(); 205 mValues.chosenSample = null; 206 207 if (target != null) { 208 // Get the sample root path and recompute the list of samples 209 String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); 210 211 File root = new File(samplesRootPath); 212 findSamplesManifests(root, root, null, null, mValues.samples); 213 214 Sdk sdk = Sdk.getCurrent(); 215 if (sdk != null) { 216 // Parse the extras to see if we can find samples that are 217 // compatible with the selected target API. 218 // First we need an SdkManager that suppresses all output. 219 SdkManager sdkman = sdk.getNewSdkManager(NullLogger.getLogger()); 220 221 Map<File, String> extras = sdkman.getExtraSamples(); 222 for (Entry<File, String> entry : extras.entrySet()) { 223 File path = entry.getKey(); 224 String name = entry.getValue(); 225 226 // Case where the sample is at the root of the directory and not 227 // in a per-sample sub-directory. 228 if (path.getName().equals(SdkConstants.FD_SAMPLE)) { 229 findSampleManifestInDir( 230 path, path, name, target.getVersion(), mValues.samples); 231 } 232 233 // Scan sub-directories 234 findSamplesManifests( 235 path, path, name, target.getVersion(), mValues.samples); 236 } 237 } 238 239 if (mValues.samples.isEmpty()) { 240 return; 241 } else { 242 Collections.sort(mValues.samples, new Comparator<Pair<String, File>>() { 243 @Override 244 public int compare(Pair<String, File> o1, Pair<String, File> o2) { 245 // Compare the display name of the sample 246 return o1.getFirst().compareTo(o2.getFirst()); 247 } 248 }); 249 } 250 251 // Try to find the old selection. 252 if (previouslyChosenSample != null) { 253 String previouslyChosenName = previouslyChosenSample.getName(); 254 for (int i = 0, n = mValues.samples.size(); i < n; i++) { 255 File file = mValues.samples.get(i).getSecond(); 256 if (file.getName().equals(previouslyChosenName)) { 257 mValues.chosenSample = file; 258 break; 259 } 260 } 261 } 262 } 263 } 264 265 /** 266 * Recursively find potential sample directories under the given directory. 267 * Actually lists any directory that contains an android manifest. 268 * Paths found are added the samplesPaths list. 269 * 270 * @param rootDir The "samples" root directory. Doesn't change during recursion. 271 * @param currDir The directory being scanned. Caller must initially set it to {@code rootDir}. 272 * @param extraName Optional name appended to the samples display name. Typically used to 273 * indicate a sample comes from a given extra package. 274 * @param targetVersion Optional target version filter. If non null, only samples that are 275 * compatible with the given target will be listed. 276 * @param samplesPaths A non-null list filled by this method with all samples found. The 277 * pair is (String: sample display name => File: sample directory). 278 */ 279 private void findSamplesManifests( 280 File rootDir, 281 File currDir, 282 @Nullable String extraName, 283 @Nullable AndroidVersion targetVersion, 284 List<Pair<String, File>> samplesPaths) { 285 if (!currDir.isDirectory()) { 286 return; 287 } 288 289 for (File f : currDir.listFiles()) { 290 if (f.isDirectory()) { 291 findSampleManifestInDir(f, rootDir, extraName, targetVersion, samplesPaths); 292 293 // Recurse in the project, to find embedded tests sub-projects 294 // We can however skip this recursion for known android sub-dirs that 295 // can't have projects, namely for sources, assets and resources. 296 String leaf = f.getName(); 297 if (!SdkConstants.FD_SOURCES.equals(leaf) && 298 !SdkConstants.FD_ASSETS.equals(leaf) && 299 !SdkConstants.FD_RES.equals(leaf)) { 300 findSamplesManifests(rootDir, f, extraName, targetVersion, samplesPaths); 301 } 302 } 303 } 304 } 305 306 private void findSampleManifestInDir( 307 File sampleDir, 308 File rootDir, 309 String extraName, 310 AndroidVersion targetVersion, 311 List<Pair<String, File>> samplesPaths) { 312 // Assume this is a sample if it contains an android manifest. 313 File manifestFile = new File(sampleDir, SdkConstants.FN_ANDROID_MANIFEST_XML); 314 if (manifestFile.isFile()) { 315 try { 316 ManifestData data = 317 AndroidManifestParser.parse(new FileWrapper(manifestFile)); 318 if (data != null) { 319 boolean accept = false; 320 if (targetVersion == null) { 321 accept = true; 322 } else if (targetVersion != null) { 323 int i = data.getMinSdkVersion(); 324 if (i != ManifestData.MIN_SDK_CODENAME) { 325 accept = i <= targetVersion.getApiLevel(); 326 } else { 327 String s = data.getMinSdkVersionString(); 328 if (s != null) { 329 accept = s.equals(targetVersion.getCodename()); 330 } 331 } 332 } 333 334 if (accept) { 335 String name = getSampleDisplayName(extraName, rootDir, sampleDir); 336 samplesPaths.add(Pair.of(name, sampleDir)); 337 } 338 } 339 } catch (Exception e) { 340 // Ignore. Don't use a sample which manifest doesn't parse correctly. 341 AdtPlugin.log(IStatus.INFO, 342 "NPW ignoring malformed manifest %s", //$NON-NLS-1$ 343 manifestFile.getAbsolutePath()); 344 } 345 } 346 } 347 348 /** 349 * Compute the sample name compared to its root directory. 350 */ 351 private String getSampleDisplayName(String extraName, File rootDir, File sampleDir) { 352 String name = null; 353 if (!rootDir.equals(sampleDir)) { 354 String path = sampleDir.getPath(); 355 int n = rootDir.getPath().length(); 356 if (path.length() > n) { 357 path = path.substring(n); 358 if (path.charAt(0) == File.separatorChar) { 359 path = path.substring(1); 360 } 361 if (path.endsWith(File.separator)) { 362 path = path.substring(0, path.length() - 1); 363 } 364 name = path.replaceAll(Pattern.quote(File.separator), " > "); //$NON-NLS-1$ 365 } 366 } 367 if (name == null && 368 rootDir.equals(sampleDir) && 369 sampleDir.getName().equals(SdkConstants.FD_SAMPLE) && 370 extraName != null) { 371 // This is an old-style extra with one single sample directory. Just use the 372 // extra's name as the same name. 373 return extraName; 374 } 375 if (name == null) { 376 // Otherwise try to use the sample's directory name as the sample name. 377 while (sampleDir != null && 378 (name == null || 379 SdkConstants.FD_SAMPLE.equals(name) || 380 SdkConstants.FD_SAMPLES.equals(name))) { 381 name = sampleDir.getName(); 382 sampleDir = sampleDir.getParentFile(); 383 } 384 } 385 if (name == null) { 386 if (extraName != null) { 387 // In the unlikely case nothing worked and we have an extra name, use that. 388 return extraName; 389 } else { 390 name = "Sample"; // fallback name... should not happen. //$NON-NLS-1$ 391 } 392 } 393 if (extraName != null) { 394 name = name + " [" + extraName + ']'; //$NON-NLS-1$ 395 } 396 397 return name; 398 } 399 400 private void validatePage() { 401 String error = null; 402 403 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADING) { 404 error = "The SDK is still loading; please wait."; 405 } 406 407 if (error == null && mValues.target == null) { 408 error = "An SDK Target must be specified."; 409 } 410 411 if (error == null && mValues.mode == Mode.SAMPLE) { 412 // Make sure this SDK target contains samples 413 if (mValues.samples == null || mValues.samples.size() == 0) { 414 error = "This target has no samples. Please select another target."; 415 } 416 } 417 418 // -- update UI & enable finish if there's no error 419 setPageComplete(error == null); 420 if (error != null) { 421 setMessage(error, IMessageProvider.ERROR); 422 } else { 423 setErrorMessage(null); 424 setMessage(null); 425 } 426 } 427 428 // ---- Implements ITargetChangeListener ---- 429 @Override 430 public void onSdkLoaded() { 431 if (mSdkTargetSelector == null) { 432 return; 433 } 434 435 // Update the sdk target selector with the new targets 436 437 // get the targets from the sdk 438 IAndroidTarget[] targets = null; 439 if (Sdk.getCurrent() != null) { 440 targets = Sdk.getCurrent().getTargets(); 441 } 442 mSdkTargetSelector.setTargets(targets); 443 444 // If there's only one target, select it. 445 // This will invoke the selection listener on the selector defined above. 446 if (targets != null && targets.length == 1) { 447 mValues.target = targets[0]; 448 mSdkTargetSelector.setSelection(mValues.target); 449 onSdkTargetModified(); 450 } else if (targets != null) { 451 // Pick the highest available platform by default (see issue #17505 452 // for related discussion.) 453 IAndroidTarget initialTarget = null; 454 for (IAndroidTarget target : targets) { 455 if (target.isPlatform() 456 && !target.getVersion().isPreview() 457 && (initialTarget == null || 458 target.getVersion().getApiLevel() > 459 initialTarget.getVersion().getApiLevel())) { 460 initialTarget = target; 461 } 462 } 463 if (initialTarget != null) { 464 mValues.target = initialTarget; 465 try { 466 mIgnore = true; 467 mSdkTargetSelector.setSelection(mValues.target); 468 } finally { 469 mIgnore = false; 470 } 471 onSdkTargetModified(); 472 } 473 } 474 475 validatePage(); 476 } 477 478 @Override 479 public void onProjectTargetChange(IProject changedProject) { 480 // Ignore 481 } 482 483 @Override 484 public void onTargetLoaded(IAndroidTarget target) { 485 // Ignore 486 } 487} 488