/* * 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.annotations.VisibleForTesting; import com.android.tools.idea.templates.TemplateUtils; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.Disposable; import com.intellij.openapi.fileChooser.FileChooserFactory; import com.intellij.openapi.fileChooser.FileSaverDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileWrapper; import com.intellij.ui.JBColor; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.text.Document; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.ArrayList; import java.util.Set; import static com.android.tools.idea.wizard.ScopedStateStore.Key; /** * ConfigureAndroidModuleStep is the first page in the New Project wizard that sets project/module name, location, and other project-global * parameters. */ public class ConfigureAndroidProjectStep extends DynamicWizardStepWithHeaderAndDescription { private static final String EXAMPLE_DOMAIN = "example.com"; public static final String SAVED_COMPANY_DOMAIN = "SAVED_COMPANY_DOMAIN"; public static final String INVALID_FILENAME_CHARS = "[/\\\\?%*:|\"<>]"; private static final CharMatcher ILLEGAL_CHARACTER_MATCHER = CharMatcher.anyOf(INVALID_FILENAME_CHARS); @VisibleForTesting static final Set<String> INVALID_MSFT_FILENAMES = ImmutableSet .of("con", "prn", "aux", "clock$", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "$mft", "$mftmirr", "$logfile", "$volume", "$attrdef", "$bitmap", "$boot", "$badclus", "$secure", "$upcase", "$extend", "$quota", "$objid", "$reparse"); protected TextFieldWithBrowseButton myProjectLocation; protected JTextField myAppName; protected JPanel myPanel; protected JTextField myCompanyDomain; protected LabelWithEditLink myPackageName; protected JLabel myProjectLocationLabel; public ConfigureAndroidProjectStep(@NotNull Disposable disposable) { this("Configure your new project", disposable); } public ConfigureAndroidProjectStep(String title, Disposable parentDisposable) { super(title, null, null, parentDisposable); setBodyComponent(myPanel); } @Override public void init() { register(WizardConstants.APPLICATION_NAME_KEY, myAppName); register(WizardConstants.COMPANY_DOMAIN_KEY, myCompanyDomain); register(WizardConstants.PACKAGE_NAME_KEY, myPackageName, new ComponentBinding<String, LabelWithEditLink>() { @Override public void setValue(@Nullable String newValue, @NotNull LabelWithEditLink component) { newValue = newValue == null ? "" : newValue; component.setText(newValue); } @Nullable @Override public String getValue(@NotNull LabelWithEditLink component) { return component.getText(); } @Nullable @Override public Document getDocument(@NotNull LabelWithEditLink component) { return component.getDocument(); } }); registerValueDeriver(WizardConstants.PACKAGE_NAME_KEY, PACKAGE_NAME_DERIVER); register(WizardConstants.PROJECT_LOCATION_KEY, myProjectLocation); registerValueDeriver(WizardConstants.PROJECT_LOCATION_KEY, myProjectLocationDeriver); myProjectLocation.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { browseForFile(); } }); myState.put(WizardConstants.APPLICATION_NAME_KEY, "My Application"); String savedCompanyDomain = PropertiesComponent.getInstance().getValue(SAVED_COMPANY_DOMAIN); if (savedCompanyDomain == null) { savedCompanyDomain = nameToPackage(System.getProperty("user.name")); if (savedCompanyDomain != null) { savedCompanyDomain = savedCompanyDomain + '.' + EXAMPLE_DOMAIN; } } if (savedCompanyDomain == null) { savedCompanyDomain = EXAMPLE_DOMAIN; } myState.put(WizardConstants.COMPANY_DOMAIN_KEY, savedCompanyDomain); super.init(); } private void browseForFile() { FileSaverDescriptor fileSaverDescriptor = new FileSaverDescriptor("Project location", "Please choose a location for your project"); File currentPath = new File(myProjectLocation.getText()); File parentPath = currentPath.getParentFile(); if (parentPath == null) { String homePath = System.getProperty("user.home"); parentPath = homePath == null ? new File("/") : new File(homePath); } VirtualFile parent = LocalFileSystem.getInstance().findFileByIoFile(parentPath); String filename = currentPath.getName(); VirtualFileWrapper fileWrapper = FileChooserFactory.getInstance().createSaveFileDialog(fileSaverDescriptor, (Project)null).save(parent, filename); if (fileWrapper != null) { myProjectLocation.setText(fileWrapper.getFile().getAbsolutePath()); } } @Override public void deriveValues(Set<Key> modified) { super.deriveValues(modified); // Save the user edited value of the company domain if (modified.contains(WizardConstants.COMPANY_DOMAIN_KEY)) { String domain = myState.get(WizardConstants.COMPANY_DOMAIN_KEY); if (domain != null && !domain.isEmpty() && myState.containsKey(WizardConstants.PACKAGE_NAME_KEY)) { @SuppressWarnings("ConstantConditions") String message = AndroidUtils.validateAndroidPackageName(myState.get(WizardConstants.PACKAGE_NAME_KEY)); if (message == null) { PropertiesComponent.getInstance().setValue(SAVED_COMPANY_DOMAIN, domain); } } } } @Override public boolean validate() { setErrorHtml(""); return validateAppName() && validatePackageName() && validateLocation(); } protected boolean validateLocation() { String projectLocation = myState.get(WizardConstants.PROJECT_LOCATION_KEY); if (projectLocation == null || projectLocation.isEmpty()) { setErrorHtml("Please specify a project location"); return false; } // Check the separators if ((File.separatorChar == '/' && projectLocation.contains("\\")) || (File.separatorChar == '\\' && projectLocation.contains("/"))) { setErrorHtml("Your project location contains incorrect slashes ('\\' vs '/')"); return false; } // Check the individual components for not allowed characters. File testFile = new File(projectLocation); while (testFile != null) { String filename = testFile.getName(); if (ILLEGAL_CHARACTER_MATCHER.matchesAnyOf(filename)) { char illegalChar = filename.charAt(ILLEGAL_CHARACTER_MATCHER.indexIn(filename)); setErrorHtml(String.format("Illegal character in project location path: '%c' in filename: %s", illegalChar, filename)); return false; } if (INVALID_MSFT_FILENAMES.contains(filename.toLowerCase())) { setErrorHtml("Illegal filename in project location path: " + filename); return false; } if (CharMatcher.WHITESPACE.matchesAnyOf(filename)) { setErrorHtml("Your project location contains whitespace. This can cause " + "problems on some platforms and is not recommended."); } if (!CharMatcher.ASCII.matchesAllOf(filename)) { setErrorHtml("Your project location contains non-ASCII characters. " + "This can cause problems on Windows. Proceed with caution."); } // Check that we can write to that location: make sure we can write into the first extant directory in the path. if (!testFile.exists() && testFile.getParentFile() != null && testFile.getParentFile().exists()) { if (!testFile.getParentFile().canWrite()) { setErrorHtml(String.format("The path '%s' is not writeable. Please choose a new location.", testFile.getParentFile().getPath())); return false; } } testFile = testFile.getParentFile(); } File file = new File(projectLocation); if (file.isFile()) { setErrorHtml("There must not already be a file at the project location"); return false; } else if (file.isDirectory() && TemplateUtils.listFiles(file).length > 0) { setErrorHtml("A non-empty directory already exists at the specified project location. " + "Existing files may be overwritten. Proceed with caution."); } if (file.getParent() == null) { setErrorHtml("The project location can not be at the filesystem root"); return false; } if (file.getParentFile().exists() && !file.getParentFile().isDirectory()) { setErrorHtml("The project location's parent directory must be a directory, not a plain file"); return false; } return true; } protected boolean validateAppName() { String appName = myState.get(WizardConstants.APPLICATION_NAME_KEY); if (appName == null || appName.isEmpty()) { setErrorHtml("Please enter an application name (shown in launcher)"); return false; } else if (Character.isLowerCase(appName.charAt(0))) { setErrorHtml("The application name for most apps begins with an uppercase letter"); } return true; } protected boolean validatePackageName() { String packageName = myState.get(WizardConstants.PACKAGE_NAME_KEY); if (packageName == null) { setErrorHtml("Please enter a package name (This package uniquely identifies your application)"); return false; } else { String message = AndroidUtils.validateAndroidPackageName(packageName); if (message != null) { setErrorHtml("Invalid package name: " + message); return false; } } return true; } @NotNull @Override public String getStepName() { return "Create Android Project"; } @Override public JComponent getPreferredFocusedComponent() { return myAppName; } @Nullable public String getHelpText(@NotNull Key<?> key) { if (key.equals(WizardConstants.APPLICATION_NAME_KEY)) { return "The application name is shown in the Play store, as well as in the Manage Applications list in Settings."; } else if (key.equals(WizardConstants.PACKAGE_NAME_KEY)) { return "The package name must be a unique identifier for your application.\n It is typically not shown to users, " + "but it <b>must</b> stay the same for the lifetime of your application; it is how multiple versions of the same application " + "are considered the \"same app\".\nThis is typically the reverse domain name of your organization plus one or more " + "application identifiers, and it must be a valid Java package name."; } else { return null; } } @VisibleForTesting static String nameToPackage(String name) { name = name.replace('-', '_'); name = name.replaceAll("[^a-zA-Z0-9_]", ""); name = name.toLowerCase(); return name; } public static final ValueDeriver<String> PACKAGE_NAME_DERIVER = new ValueDeriver<String>() { @Nullable @Override public Set<Key<?>> getTriggerKeys() { return makeSetOf(WizardConstants.APPLICATION_NAME_KEY, WizardConstants.COMPANY_DOMAIN_KEY); } @Nullable @Override public String deriveValue(@NotNull ScopedStateStore state, Key changedKey, @Nullable String currentValue) { String projectName = state.get(WizardConstants.APPLICATION_NAME_KEY); if (projectName == null) { projectName = "app"; } projectName = nameToPackage(projectName); String companyDomain = state.get(WizardConstants.COMPANY_DOMAIN_KEY); if (companyDomain == null) { companyDomain = EXAMPLE_DOMAIN; } ArrayList domainParts = Lists.newArrayList(companyDomain.split("\\.")); String reversedDomain = Joiner.on('.').skipNulls().join(Lists.reverse(Lists.transform(domainParts, new Function<String, String>() { @Override public String apply(String input) { String name = nameToPackage(input); return name.isEmpty() ? null : name; } }))); return reversedDomain + '.' + projectName; } }; private static final ValueDeriver<String> myProjectLocationDeriver = new ValueDeriver<String>() { @Nullable @Override public Set<Key<?>> getTriggerKeys() { return makeSetOf(WizardConstants.APPLICATION_NAME_KEY); } @Nullable @Override public String deriveValue(ScopedStateStore state, Key changedKey, @Nullable String currentValue) { String name = state.get(WizardConstants.APPLICATION_NAME_KEY); name = name == null ? "" : name; name = name.replaceAll(INVALID_FILENAME_CHARS, ""); name = name.replaceAll("\\s", ""); File baseDirectory = new File(NewProjectWizardState.getProjectFileDirectory()); File projectDir = new File(baseDirectory, name); int i = 2; while (projectDir.exists()) { projectDir = new File(baseDirectory, name + i); i++; } return projectDir.getPath(); } }; @Nullable @Override protected JComponent getHeader() { return ConfigureAndroidProjectPath.buildConfigurationHeader(); } @Override @Nullable protected JBColor getTitleTextColor() { return WizardConstants.ANDROID_NPW_TITLE_COLOR; } }