blob: b4ff017cba8968c30807fd83e8d32f24133efbcb [file] [log] [blame]
/*
* Copyright (C) 2012 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.motorola.studio.android.model;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jface.wizard.IWizardContainer;
import org.eclipse.osgi.util.NLS;
import com.android.sdklib.IAndroidTarget;
import com.motorola.studio.android.AndroidPlugin;
import com.motorola.studio.android.adt.Sample;
import com.motorola.studio.android.adt.SdkUtils;
import com.motorola.studio.android.common.IAndroidConstants;
import com.motorola.studio.android.common.exception.AndroidException;
import com.motorola.studio.android.common.utilities.AndroidStatus;
import com.motorola.studio.android.common.utilities.EclipseUtils;
import com.motorola.studio.android.i18n.AndroidNLS;
/**
* Class that implements the model for the new project wizard
*/
public class AndroidProject implements IWizardModel
{
private static final IWorkspace WORKSPACE = ResourcesPlugin.getWorkspace();
private static final String SDK_VERSION = "1.5"; //$NON-NLS-1$
private static final String BIN_FOLDER = "bin"; //$NON-NLS-1$
private static final String CLASS_EXTENSION = ".class"; //$NON-NLS-1$
private static final String R_DRAWABLE_CLASS = "R$drawable" + CLASS_EXTENSION; //$NON-NLS-1$
private static final String SETTINGS_FOLDER = ".settings"; //$NON-NLS-1$
private static final String SETTINGS_FILE = "org.eclipse.core.resources.prefs"; //$NON-NLS-1$
public static final String ANDROID_NATURE = "com.android.ide.eclipse.adt.AndroidNature"; //$NON-NLS-1$
private static final int MAX_PATH_LENGTH = 255;
/**
* Represents the Type of new Projects
*/
public static enum SourceTypes
{
SAMPLE, EXISTING, NEW, WIDGET
};
/*
* (non-Javadoc)
*
* @see java.lang.Object#finalize()
*/
@Override
public void finalize() throws Throwable
{
if (listener != null)
{
AndroidPlugin.getDefault().removeSDKLoaderListener(listener);
}
super.finalize();
}
private SourceTypes sourceType = SourceTypes.NEW;
private Sample sample = null;
private String location = null;
private boolean useDefaultLocation = true;
private boolean addNativeSupport = false;
private boolean needToObfuscate = false;
/**
* Return the Type of the new Project
*
* @return
*/
public SourceTypes getSourceType()
{
return sourceType;
}
/**
* Change the Type of the new Project
*
* @param sourceType
*/
public void setSourceType(SourceTypes sourceType)
{
this.sourceType = sourceType;
}
/**
* Check if adding native support
*
* @return
*/
public boolean isAddingNativeSupport()
{
return addNativeSupport;
}
/**
* Set add native support property
*
* @param hasNativeSupport
*/
public void setAddNativeSupport(boolean addNativeSupport)
{
this.addNativeSupport = addNativeSupport;
}
public void setNeedToObfuscate(boolean needToObfuscate)
{
this.needToObfuscate = needToObfuscate;
}
public boolean needToObfuscate()
{
return needToObfuscate;
}
/**
* Returns the Project location.
*
* @see #isUsingDefaultLocation()
* @return
*/
public String getLocation()
{
return location;
}
/**
* Return the sample to use
*
* @see #getSourceType()
* @return
*/
public Sample getSample()
{
return sample;
}
/**
* Change the used sample
*
* @see #getSourceType()
* @param sample
*/
public void setSample(Sample sample)
{
this.sample = sample;
}
/**
* Change the Project Location
*
* @param location
*/
public void setLocation(String location)
{
this.location = location;
}
/**
* Check if using the default location
*
* @return
*/
public boolean isUsingDefaultLocation()
{
return useDefaultLocation;
}
/**
* Set use default location property
*
* @param useDefaultLocation
*/
public void setUseDefaultLocation(boolean useDefaultLocation)
{
this.useDefaultLocation = useDefaultLocation;
}
private String minSdkVersion = null;
private String name = ""; //$NON-NLS-1$
private String activityName = ""; //$NON-NLS-1$
private String packageName = ""; //$NON-NLS-1$
private IAndroidTarget sdkTarget = null;
private List<String> sourceFolders = null;
private boolean usingDefaultPackage = true;
private final String PACKAGE_ROOT = "com."; //$NON-NLS-1$
private Runnable listener = null;
private String appName;
/**
* Check if using the defaul package name
*
* @return
*/
public boolean isUsingDefaultPackage()
{
return usingDefaultPackage;
}
/**
* Set if using the Default package name
*
* @param usingDefaultPackage
*/
public void setUsingDefaultPackage(boolean usingDefaultPackage)
{
this.usingDefaultPackage = usingDefaultPackage;
}
/**
* Creates a new AndroidProject
*/
public AndroidProject()
{
// initialize SDK Targets
if (SdkUtils.getCurrentSdk() == null)
{
// this listener will be called when a SDK is configured
listener = new Runnable()
{
public void run()
{
IAndroidTarget[] targets = SdkUtils.getAllTargets();
if ((targets != null) && (targets.length > 0))
{
int maxApiVersion = -1;
for (IAndroidTarget aTarget : targets)
{
int apiVersion = aTarget.getVersion().getApiLevel();
if (maxApiVersion < apiVersion)
{
sdkTarget = aTarget;
maxApiVersion = apiVersion;
}
}
// set the API string as the min SDK version - this is the way ADT does
minSdkVersion = sdkTarget.getVersion().getApiString();
}
AndroidPlugin.getDefault().removeSDKLoaderListener(listener);
listener = null;
}
};
AndroidPlugin.getDefault().addSDKLoaderListener(listener);
}
else
{
IAndroidTarget[] targets = SdkUtils.getAllTargets();
if ((targets != null) && (targets.length > 0))
{
int maxApiVersion = -1;
for (IAndroidTarget aTarget : targets)
{
int apiVersion = aTarget.getVersion().getApiLevel();
if (maxApiVersion < apiVersion)
{
sdkTarget = aTarget;
maxApiVersion = apiVersion;
}
}
// set the API string as the min SDK version - this is the way ADT does
minSdkVersion = sdkTarget.getVersion().getApiString();
}
}
sourceFolders = new ArrayList(3);
sourceFolders.add(IAndroidConstants.FD_SOURCES);
sourceFolders.add(IAndroidConstants.FD_GEN_SOURCES);
}
/**
* Return the Default Package Name.
*
* @return
*/
private String getDefaultPackageName()
{
return PACKAGE_ROOT + name2package(name);
}
/**
* Return the default name for activities.
*
* @return
*/
public String getDefaultActivityName()
{
String activityName = name;
activityName = activityName.replaceAll("^[0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
activityName = activityName.replaceAll("[\\.]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
return activityName;
}
/**
* Returns a valid package name from the Project Name
*
* @param name
* @return
*/
protected String name2package(String name)
{
String packageName = getDefaultActivityName().toLowerCase();
packageName = packageName.replaceAll("[^A-Za-z0-9_]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
if (packageName.endsWith("_") && (packageName.length() > 1)) //$NON-NLS-1$
{
packageName = packageName.substring(0, packageName.length() - 1);
}
return packageName;
}
/**
* Return the Minimum SDK Version
*
* @return
*/
public String getMinSdkVersion()
{
return minSdkVersion;
}
/**
* Returns <code>true</code> in case the SDK is a preview,
* <code>false</code> otherwise.
*
* @return Returns <code>true</code> in case the SDK is a preview,
* <code>false</code> otherwise.
*/
public boolean isSdkPreview()
{
return getSdkTarget().getVersion().isPreview();
}
/**
* Return the project Name
*
* @return
*/
public String getName()
{
return name;
}
/**
* Return the package name. Or the Default package name, if using it.
*
* @return
*/
public String getPackageName()
{
String packageName;
if (isUsingDefaultPackage())
{
packageName = getDefaultPackageName();
}
else
{
packageName = this.packageName;
}
return packageName;
}
/**
* Return the SDK target used by this project.
*
* @return
*/
public IAndroidTarget getSdkTarget()
{
return sdkTarget;
}
/**
* Return the Source Folder to be created in the project.
*
* @return
*/
public List<String> getSourceFolders()
{
return sourceFolders;
}
/*
* (non-Javadoc)
*
* @see com.motorola.studio.android.model.IWizardModel#getStatus()
*/
public IStatus getStatus()
{
IStatus status = Status.OK_STATUS;
// Validating Project Name
if ((name == null) || (name.length() == 0))
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_ProjectNameMustBeSpecified);
}
else
{
Pattern pattern = Pattern.compile("([A-Za-z0-9_\\.])+"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(name);
if (!matcher.matches())
{
String errMsg = NLS.bind(AndroidNLS.ERR_AndroidProject_InvalidProjectName, name);
status = new AndroidStatus(IStatus.ERROR, errMsg);
}
else if (WORKSPACE.getRoot().getProject(name).exists())
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_DuplicatedProjectNameInWorkspace);
}
else
{
status = WORKSPACE.validateName(name, IResource.PROJECT);
}
}
if (status.isOK() && (sourceType == SourceTypes.EXISTING))
{
// Check first if the path is empty. If true, we show a info
// message to the user
if (getLocation().length() == 0)
{
status =
new AndroidStatus(IStatus.INFO,
AndroidNLS.ERR_AndroidProject_EmptySourceLocation);
}
else
{
// Check if the manifest exists
Path path = new Path(getLocation());
String osPath = path.append(IAndroidConstants.FN_ANDROID_MANIFEST).toOSString();
File manifest = new File(osPath);
if (!manifest.exists() || !manifest.isFile())
{
String errMsg =
NLS.bind(AndroidNLS.ERR_AndroidProject_FileNotFoundError,
IAndroidConstants.FN_ANDROID_MANIFEST, path.lastSegment());
status = new AndroidStatus(IStatus.ERROR, errMsg);
}
}
}
// Validating Project Location
if (status.isOK() && !isUsingDefaultLocation())
{
IProject handle = WORKSPACE.getRoot().getProject(name);
status = WORKSPACE.validateProjectLocation(handle, new Path(location));
if (status.isOK() && isNewProject() && (getLocation() != null))
{
boolean projectLocationIsEmpty =
ProjectCreationSupport.validateNewProjectLocationIsEmpty(new Path(
getLocation()));
if (!projectLocationIsEmpty)
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.UI_ProjectCreationSupport_NonEmptyFolder);
}
}
}
// Validating Project Location when adding Native Support on Windows
if (status.isOK() && isAddingNativeSupport() && Platform.getOS().equals(Platform.OS_WIN32))
{
if ((isUsingDefaultLocation() && ResourcesPlugin.getWorkspace().getRoot().getLocation()
.toOSString().contains(" ")) //$NON-NLS-1$
|| (!isUsingDefaultLocation() && location.contains(" "))) //$NON-NLS-1$
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_LocationContainsWhitespaces);
}
}
// Validating if the project could create a "file name too long"
// structure
if (status.isOK())
{
String basePath = isUsingDefaultLocation() ? getDefaultLocation() : getLocation();
String relativePackagePath = getPackageName().replace('.', File.separatorChar);
String binPath =
basePath + File.separatorChar + BIN_FOLDER + File.separatorChar
+ relativePackagePath;
String rDrawableBinClass = binPath + File.separatorChar + R_DRAWABLE_CLASS;
String settingsFilePath =
basePath + File.separatorChar + SETTINGS_FOLDER + File.separatorChar
+ SETTINGS_FILE;
String defaultClassBin =
binPath + File.separator + getDefaultActivityName() + CLASS_EXTENSION;
int maxFilenameLength = getMax(new int[]
{
rDrawableBinClass.length(), settingsFilePath.length(), defaultClassBin.length()
});
if (maxFilenameLength > MAX_PATH_LENGTH)
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_ProjectNameTooLong);
}
}
// Validating SDK Target
if (status.isOK() && (sdkTarget == null))
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_ASdkTargetMustBeSpecified);
}
// Validates the application name
if (status.isOK()
&& ((sourceType == SourceTypes.NEW) || (sourceType == SourceTypes.WIDGET)))
{
if (appName.trim().length() == 0)
{
status =
new AndroidStatus(IStatus.WARNING,
AndroidNLS.WARN_AndroidProject_ApplicationNameIsEmpty);
}
if (appName.contains("&")) //$NON-NLS-1$
{
status =
new AndroidStatus(IStatus.ERROR, NLS.bind(
AndroidNLS.ERR_AndroidProject_InvalidApplicationName, appName));
}
}
// Validating PackageName
if (status.isOK()
&& ((sourceType == SourceTypes.NEW) || (sourceType == SourceTypes.WIDGET))
&& !isUsingDefaultPackage())
{
if (getPackageName().length() == 0)
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_EmptyPackageName);
}
else if (status.isOK())
{
status =
JavaConventions.validatePackageName(getPackageName(), SDK_VERSION,
SDK_VERSION);
}
if (status.isOK() && (getPackageName().indexOf('.') == -1))
{
// The Android Activity Manager does not accept packages names
// with only one
// identifier. Check the package name has at least one dot in
// them (the previous rule
// validated that if such a dot exist, it's not the first nor
// last characters of the
// string.)
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_InvalidPackageName);
}
if (status.isOK())
{
Pattern pattern = Pattern.compile("[A-Za-z0-9_\\.]+"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(getPackageName());
if (!matcher.matches())
{
String errMsg =
NLS.bind(AndroidNLS.ERR_AndroidProject_InvalidCharsInPackageName,
getPackageName());
status = new AndroidStatus(IStatus.ERROR, errMsg);
}
}
}
// validating activity name
if ((this.getSourceType() == SourceTypes.NEW)
|| (this.getSourceType() == SourceTypes.WIDGET))
{
if ((activityName == null) || (activityName.length() == 0))
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_ActivityNameMustBeSpecified);
}
else
{
String onlyChars = "[A-Za-z_]"; //$NON-NLS-1$
Pattern pattern = Pattern.compile("(" + onlyChars + ")(\\w)+"); //$NON-NLS-1$ //$NON-NLS-2$
Matcher matcher = pattern.matcher(activityName);
if (!matcher.matches())
{
String errMsg =
NLS.bind(AndroidNLS.ERR_AndroidProject_InvalidActivityName,
activityName);
status = new AndroidStatus(IStatus.ERROR, errMsg);
}
}
}
// Validating Min Sdk Version
if (status.isOK()
&& ((sourceType == SourceTypes.NEW) || (sourceType == SourceTypes.WIDGET)))
{
// validate in case the sdk is preview
if (isSdkPreview())
{
String sdkAPI = getSdkTarget().getVersion().getApiString();
if (!sdkAPI.equals(getMinSdkVersion()))
{
status =
new AndroidStatus(IStatus.ERROR, NLS.bind(
AndroidNLS.AndroidProject_MsgSDKVersionIsPreview, sdkAPI));
}
}
// since it is not a preview, validate it normally
else
{
int version = -1;
try
{
// If not empty, it must be a valid integer > 0
version = Integer.parseInt(getMinSdkVersion());
}
catch (NumberFormatException nfe)
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_InvalidSdkVersion);
}
if (status.isOK() && (version < 1))
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.ERR_AndroidProject_InvalidSdkVersion);
}
if (status.isOK() && (getSdkTarget() != null))
{
if (getSdkTarget().getVersion().getApiLevel() > version)
{
status =
new AndroidStatus(IStatus.WARNING,
AndroidNLS.ERR_AndroidProject_InvalidApiLevel);
}
else if (getSdkTarget().getVersion().getApiLevel() < version)
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.EXC_AndroidProject_InvalidMinimumSdkVersion);
}
}
}
}
//validate if there are available samples
if (status.isOK() && (sourceType == SourceTypes.SAMPLE) && (sdkTarget != null)
&& (SdkUtils.getSamples(sdkTarget).length == 0))
{
status =
new AndroidStatus(IStatus.ERROR,
AndroidNLS.EXC_AndroidProject_NoSamplesAvailable);
}
// Validating Project Location when adding Obfuscation Support
if (status.isOK() && needToObfuscate())
{
if ((isUsingDefaultLocation() && ResourcesPlugin.getWorkspace().getRoot().getLocation()
.toOSString().contains(" ")) //$NON-NLS-1$
|| (!isUsingDefaultLocation() && location.contains(" "))) //$NON-NLS-1$
{
status =
new AndroidStatus(IStatus.WARNING,
AndroidNLS.WRN_Obfuscation_ProjectLocationContainWhitespaces);
}
}
return status;
}
/*
* (non-Javadoc)
*
* @see com.motorola.studio.android.model.IWizardModel#needMoreInformation()
*/
public boolean needMoreInformation()
{
boolean needMoreInformation =
(name == null) || (name.length() == 0)
|| !WORKSPACE.validateName(name, IResource.PROJECT).isOK();
if (!needMoreInformation)
{
boolean projectPathInformation =
(location == null)
|| ((location.length() == 0) || !WORKSPACE.validateProjectLocation(
WORKSPACE.getRoot().getProject(name), new Path(location))
.isOK());
boolean newProjectInformation = !useDefaultLocation && projectPathInformation;
switch (sourceType)
{
case NEW:
needMoreInformation = newProjectInformation;
break;
case SAMPLE:
needMoreInformation = newProjectInformation || (sample == null);
break;
case EXISTING:
needMoreInformation = projectPathInformation;
break;
case WIDGET:
needMoreInformation = newProjectInformation;
}
}
if (!needMoreInformation)
{
if (((sourceType == SourceTypes.NEW) || (sourceType == SourceTypes.WIDGET))
&& !isUsingDefaultPackage())
{
needMoreInformation =
(getPackageName().length() == 0)
|| !JavaConventions.validatePackageName(getPackageName(),
SDK_VERSION, SDK_VERSION).isOK()
|| (getPackageName().indexOf('.') == -1);
}
}
if (!needMoreInformation)
{
needMoreInformation = sdkTarget == null;
}
return needMoreInformation;
}
/* (non-Javadoc)
* @see com.motorola.studio.android.model.IWizardModel#save(org.eclipse.jface.wizard.IWizardContainer, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean save(IWizardContainer container, IProgressMonitor monitor)
{
try
{
return ProjectCreationSupport.createProject(this, container);
}
catch (AndroidException e)
{
IStatus status =
new Status(IStatus.ERROR, AndroidPlugin.PLUGIN_ID, e.getLocalizedMessage());
EclipseUtils.showErrorDialog(AndroidNLS.GEN_Error,
AndroidNLS.EXC_AndroidProject_AnErrorHasOccurredWhenCreatingTheProject, status);
return false;
}
}
/**
* Change the SDK
*
* @param minSdkVersion
*/
public void setMinSdkVersion(String minSdkVersion)
{
this.minSdkVersion = minSdkVersion;
}
/**
* Change the project name
*
* @param name
*/
public void setName(String name)
{
this.name = name.trim();
}
/**
* Change the package name. If using default package name will throw an
* {@link IllegalStateException}
*
* @param packageName
*/
public void setPackageName(String packageName)
{
if (isUsingDefaultPackage())
{
throw new IllegalStateException();
}
this.packageName = packageName.trim();
}
/**
* Change the SDK Target
*
* @param sdkTarget
*/
public void setSdkTarget(IAndroidTarget sdkTarget)
{
this.sdkTarget = sdkTarget;
}
/**
* Change the Source Folder
*
* @param sourceFolder
*/
public void setSourceFolder(List<String> sourceFolders)
{
this.sourceFolders.clear();
this.sourceFolders.addAll(sourceFolders);
}
/**
* Return the Default location for this project
*
* @return
*/
public String getDefaultLocation()
{
return ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString() + File.separator
+ name;
}
/**
* Check if this project is a new project.
*
* @return
*/
public boolean isNewProject()
{
return getSourceType() != SourceTypes.EXISTING;
}
/**
* Set the name of the activity being created with the project
* This attribute is only valid for New Projects.
* Other kind of project will ignore this attribute.
*/
public void setActivityName(String activityName)
{
this.activityName = activityName;
}
/**
* Return this project default activity name
* @return an Activity name as string
*/
public String getActivityName()
{
return this.activityName;
}
/**
* Retrieves the maximum value from an int array
*
* @param values
* the int array
*
* @return the maximum value from the int array
*/
private int getMax(int[] values)
{
int max = values[0];
for (int i = 1; i < values.length; i++)
{
max = Math.max(max, values[i]);
}
return max;
}
/**
* Set the application name to be used
* The app name will be used only when the project is new
* @param app_name
*/
public void setApplicationName(String app_name)
{
this.appName = app_name;
}
/**
* get the user defined application name
* @return
*/
public String getApplicationName()
{
return this.appName;
}
}