Merge commit 'korg/cupcake'

This commit is contained in:
The Android Open Source Project
2009-03-27 15:30:35 -07:00
97 changed files with 8553 additions and 1213 deletions

View File

@@ -40,7 +40,10 @@ Require-Bundle: com.android.ide.eclipse.ddms,
org.eclipse.wst.sse.ui,
org.eclipse.wst.xml.core,
org.eclipse.wst.xml.ui,
org.eclipse.jdt.junit
org.eclipse.jdt.junit,
org.eclipse.jdt.junit.runtime,
org.eclipse.ltk.core.refactoring,
org.eclipse.ltk.ui.refactoring
Eclipse-LazyStart: true
Export-Package: com.android.ide.eclipse.adt,
com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests",

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

@@ -470,7 +470,7 @@
point="org.eclipse.ui.actionSets">
<actionSet
description="Android Wizards"
id="adt.actionSet1"
id="adt.actionSet.wizards"
label="Android Wizards"
visible="true">
<action
@@ -481,12 +481,6 @@
style="push"
toolbarPath="android_project"
tooltip="Opens a wizard to help create a new Android XML file">
<enablement>
<objectState
name="projectNature"
value="com.android.ide.eclipse.adt.AndroidNature">
</objectState>
</enablement>
</action>
<action
class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
@@ -498,6 +492,21 @@
tooltip="Opens a wizard to help create a new Android project">
</action>
</actionSet>
<actionSet
description="Refactorings for Android"
id="adt.actionSet.refactorings"
label="Android Refactorings"
visible="true">
<action
class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringAction"
definitionId="com.android.ide.eclipse.adt.refactoring.extract.string"
id="com.android.ide.eclipse.adt.actions.ExtractString"
label="Extract Android String..."
menubarPath="org.eclipse.jdt.ui.refactoring.menu/codingGroup"
style="push"
tooltip="Extracts a string into Android resource string">
</action>
</actionSet>
</extension>
<extension
point="org.eclipse.debug.core.launchDelegates">
@@ -506,8 +515,84 @@
delegateDescription="Removes the Android JAR from the Bootstrap Classpath"
id="com.android.ide.eclipse.adt.launch.JUnitLaunchConfigDelegate.launchAndroidJunit"
modes="run,debug"
name="Android JUnit"
name="Android JUnit Test"
type="org.eclipse.jdt.junit.launchconfig">
</launchDelegate>
</extension>
<extension
point="org.eclipse.debug.core.launchConfigurationTypes">
<launchConfigurationType
delegate="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchConfigDelegate"
id="com.android.ide.eclipse.adt.junit.launchConfigurationType"
modes="run,debug"
name="Android JUnit Test"
public="true"
sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"
sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer">
</launchConfigurationType>
</extension>
<extension
point="org.eclipse.debug.ui.launchConfigurationTypeImages">
<launchConfigurationTypeImage
configTypeID="com.android.ide.eclipse.adt.junit.launchConfigurationType"
icon="icons/androidjunit.png"
id="com.android.ide.eclipse.adt.junit.launchConfigurationTypeImage">
</launchConfigurationTypeImage>
</extension>
<extension
point="org.eclipse.debug.ui.launchConfigurationTabGroups">
<launchConfigurationTabGroup
class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitTabGroup"
description="Android JUnit Test"
id="com.android.ide.eclipse.adt.junit.AndroidJUnitLaunchConfigTabGroup"
type="com.android.ide.eclipse.adt.junit.launchConfigurationType"/>
</extension>
<extension
point="org.eclipse.debug.ui.launchShortcuts">
<shortcut
class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchShortcut"
icon="icons/android.png"
id="com.android.ide.eclipse.adt.junit.launchShortcut"
label="Android JUnit Test"
modes="run,debug">
<contextualLaunch>
<enablement>
<with variable="selection">
<count value="1"/>
<iterate>
<adapt type="org.eclipse.jdt.core.IJavaElement">
<test property="org.eclipse.jdt.core.isInJavaProjectWithNature" value="com.android.ide.eclipse.adt.AndroidNature"/>
<test property="org.eclipse.jdt.core.hasTypeOnClasspath" value="junit.framework.Test"/>
<test property="org.eclipse.jdt.junit.canLaunchAsJUnit" forcePluginActivation="true"/>
</adapt>
</iterate>
</with>
</enablement>
</contextualLaunch>
<configurationType
id="com.android.ide.eclipse.adt.junit.launchConfigurationType">
</configurationType>
</shortcut>
</extension>
<extension
point="org.eclipse.ui.commands">
<category
description="Refactorings for Android Projects"
id="com.android.ide.eclipse.adt.refactoring.category"
name="Android Refactorings">
</category>
<command
categoryId="com.android.ide.eclipse.adt.refactoring.category"
description="Extract Strings into Android String Resources"
id="com.android.ide.eclipse.adt.refactoring.extract.string"
name="Extract Android String">
</command>
</extension>
<extension
point="org.eclipse.ltk.core.refactoring.refactoringContributions">
<contribution
class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringContribution"
id="com.android.ide.eclipse.adt.refactoring.extract.string">
</contribution>
</extension>
</plugin>

View File

@@ -1040,6 +1040,16 @@ public class AdtPlugin extends AbstractUIPlugin {
mSdkIsLoaded = LoadStatus.LOADED;
progress.setTaskName("Check Projects");
ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
for (IJavaProject javaProject : mPostLoadProjectsToResolve) {
if (javaProject.getProject().isOpen()) {
list.add(javaProject);
}
}
// done with this list.
mPostLoadProjectsToResolve.clear();
// check the projects that need checking.
// The method modifies the list (it removes the project that
@@ -1047,14 +1057,13 @@ public class AdtPlugin extends AbstractUIPlugin {
AndroidClasspathContainerInitializer.checkProjectsCache(
mPostLoadProjectsToCheck);
mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck);
list.addAll(mPostLoadProjectsToCheck);
// update the project that needs recompiling.
if (mPostLoadProjectsToResolve.size() > 0) {
IJavaProject[] array = mPostLoadProjectsToResolve.toArray(
new IJavaProject[mPostLoadProjectsToResolve.size()]);
if (list.size() > 0) {
IJavaProject[] array = list.toArray(
new IJavaProject[list.size()]);
AndroidClasspathContainerInitializer.updateProjects(array);
mPostLoadProjectsToResolve.clear();
}
progress.worked(10);
@@ -1273,7 +1282,7 @@ public class AdtPlugin extends AbstractUIPlugin {
AdtPlugin.PLUGIN_ID,
UNKNOWN_EDITOR);
try {
file.setPersistentProperty(qname, "1");
file.setPersistentProperty(qname, "1"); //$NON-NLS-1$
} catch (CoreException e) {
// pass
}

View File

@@ -196,7 +196,7 @@ public class ApkBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {
@@ -979,7 +979,10 @@ public class ApkBuilder extends BaseBuilder {
writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list);
for (IJavaProject referencedJavaProject : referencedJavaProjects) {
if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE)) {
// only include output from non android referenced project
// (This is to handle the case of reference Android projects in the context of
// instrumentation projects that need to reference the projects to be tested).
if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE) == false) {
writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list);
}
}
@@ -1084,7 +1087,10 @@ public class ApkBuilder extends BaseBuilder {
IWorkspaceRoot wsRoot = ws.getRoot();
for (IJavaProject javaProject : referencedJavaProjects) {
if (javaProject.getProject().hasNature(AndroidConstants.NATURE)) {
// only include output from non android referenced project
// (This is to handle the case of reference Android projects in the context of
// instrumentation projects that need to reference the projects to be tested).
if (javaProject.getProject().hasNature(AndroidConstants.NATURE) == false) {
// get the output folder
IPath path = null;
try {

View File

@@ -197,7 +197,7 @@ public class PreCompilerBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {
@@ -222,6 +222,7 @@ public class PreCompilerBuilder extends BaseBuilder {
PreCompilerDeltaVisitor dv = null;
String javaPackage = null;
int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
if (kind == FULL_BUILD) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
@@ -253,6 +254,7 @@ public class PreCompilerBuilder extends BaseBuilder {
// get the java package from the visitor
javaPackage = dv.getManifestPackage();
minSdkVersion = dv.getMinSdkVersion();
}
}
@@ -276,7 +278,7 @@ public class PreCompilerBuilder extends BaseBuilder {
if (manifest == null) {
String msg = String.format(Messages.s_File_Missing,
AndroidConstants.FN_ANDROID_MANIFEST);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
AdtPlugin.printErrorToConsole(project, msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// This interrupts the build. The next builders will not run.
@@ -304,19 +306,34 @@ public class PreCompilerBuilder extends BaseBuilder {
// get the java package from the parser
javaPackage = parser.getPackage();
minSdkVersion = parser.getApiLevelRequirement();
}
if (javaPackage == null || javaPackage.length() == 0) {
// looks like the AndroidManifest file isn't valid.
String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
AndroidConstants.FN_ANDROID_MANIFEST);
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
msg);
if (minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK &&
minSdkVersion < projectTarget.getApiVersionNumber()) {
// check it against the target api level
String msg = String.format(
"Manifest min SDK version (%1$d) is lower than project target API level (%2$d)",
minSdkVersion, projectTarget.getApiVersionNumber());
AdtPlugin.printErrorToConsole(project, msg);
BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg,
IMarker.SEVERITY_ERROR);
// This interrupts the build. The next builders will not run.
stopBuild(msg);
}
if (javaPackage == null || javaPackage.length() == 0) {
// looks like the AndroidManifest file isn't valid.
String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
AndroidConstants.FN_ANDROID_MANIFEST);
AdtPlugin.printErrorToConsole(project, msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// This interrupts the build. The next builders will not run.
stopBuild(msg);
}
// at this point we have the java package. We need to make sure it's not a different
// package than the previous one that were built.
if (javaPackage.equals(mManifestPackage) == false) {

View File

@@ -72,8 +72,10 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
/** Manifest check/parsing flag. */
private boolean mCheckedManifestXml = false;
/** Application Pacakge, gathered from the parsing of the manifest */
/** Application Package, gathered from the parsing of the manifest */
private String mJavaPackage = null;
/** minSDKVersion attribute value, gathered from the parsing of the manifest */
private int mMinSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
// Internal usage fields.
/**
@@ -137,6 +139,22 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
return mJavaPackage;
}
/**
* Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
* <p/>
* This can return {@link AndroidManifestParser#INVALID_MIN_SDK} in two cases:
* <ul>
* <li>The manifest was not part of the resource change delta, and the manifest was
* not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
* <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
* but the package declaration is missing</li>
* </ul>
* @return the minSdkVersion or {@link AndroidManifestParser#INVALID_MIN_SDK}.
*/
public int getMinSdkVersion() {
return mMinSdkVersion;
}
/*
* (non-Javadoc)
*
@@ -184,6 +202,7 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
if (parser != null) {
mJavaPackage = parser.getPackage();
mMinSdkVersion = parser.getApiLevelRequirement();
}
mCheckedManifestXml = true;

View File

@@ -55,7 +55,7 @@ public class ResourceManagerBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {

View File

@@ -32,16 +32,41 @@ public class AndroidLaunchConfiguration {
*/
public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
public static final boolean AUTO_TARGET_MODE = true;
/**
* Target selection mode for the configuration: either {@link #AUTO} or {@link #MANUAL}.
*/
public enum TargetMode {
/** Automatic target selection mode. */
AUTO(true),
/** Manual target selection mode. */
MANUAL(false);
private boolean mValue;
TargetMode(boolean value) {
mValue = value;
}
public boolean getValue() {
return mValue;
}
public static TargetMode getMode(boolean value) {
for (TargetMode mode : values()) {
if (mode.mValue == value) {
return mode;
}
}
return null;
}
}
/**
* Target selection mode.
* <ul>
* <li><code>true</code>: automatic mode, see {@link #AUTO_TARGET_MODE}</li>
* <li><code>false</code>: manual mode</li>
* </ul>
* @see TargetMode
*/
public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
public TargetMode mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
/**
* Indicates whether the emulator should be called with -wipe-data
@@ -81,8 +106,9 @@ public class AndroidLaunchConfiguration {
}
try {
mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
mTargetMode);
boolean value = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
mTargetMode.getValue());
mTargetMode = TargetMode.getMode(value);
} catch (CoreException e) {
// nothing to be done here, we'll use the default value
}

View File

@@ -17,9 +17,6 @@
package com.android.ide.eclipse.adt.launch;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Client;
import com.android.ddmlib.ClientData;
import com.android.ddmlib.Device;
@@ -27,13 +24,19 @@ import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.SyncService.SyncResult;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo.InstallRetryMode;
import com.android.ide.eclipse.adt.launch.DeviceChooserDialog.DeviceChooserResponse;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.avd.AvdManager;
@@ -52,6 +55,8 @@ import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMConnector;
import org.eclipse.jdt.launching.JavaRuntime;
@@ -64,6 +69,7 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -94,8 +100,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
/**
* List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
* <p>Once an emulator has connected, {@link DelayedLaunchInfo#mDevice} is set and the
* DelayedLaunchInfo object is moved to {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
* <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
* DelayedLaunchInfo object is moved to
* {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
* <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
*/
private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
@@ -236,7 +243,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
// set default target mode
wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
LaunchConfigDelegate.DEFAULT_TARGET_MODE);
LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());
// default AVD: None
wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);
@@ -307,7 +314,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
* <code>DEBUG_MODE</code>.
* @param apk the resource to the apk to launch.
* @param debuggable the debuggable value of the app, or null if not set.
* @param requiredApiVersionNumber the api version required by the app, or -1 if none.
* @param requiredApiVersionNumber the api version required by the app, or
* {@link AndroidManifestParser#INVALID_MIN_SDK} if none.
* @param launchAction the action to perform after app sync
* @param config the launch configuration
* @param launch the launch object
@@ -331,6 +339,16 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
Sdk currentSdk = Sdk.getCurrent();
AvdManager avdManager = currentSdk.getAvdManager();
// reload the AVDs to make sure we are up to date
try {
avdManager.reloadAvds();
} catch (AndroidLocationException e1) {
// this happens if the AVD Manager failed to find the folder in which the AVDs are
// stored. This is unlikely to happen, but if it does, we should force to go manual
// to allow using physical devices.
config.mTargetMode = TargetMode.MANUAL;
}
// get the project target
final IAndroidTarget projectTarget = currentSdk.getTarget(project);
@@ -355,7 +373,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
* If == 1, launch the application on this AVD/device.
*/
if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) {
if (config.mTargetMode == TargetMode.AUTO) {
// if we are in automatic target mode, we need to find the current devices
IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
@@ -468,7 +486,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
// FIXME: ask the user if he wants to create a AVD.
// we found no compatible AVD.
AdtPlugin.printErrorToConsole(project, String.format(
"Failed to find a AVD compatible with target '%1$s'. Launch aborted.",
"Failed to find an AVD compatible with target '%1$s'. Launch aborted.",
projectTarget.getName()));
stopLaunch(launchInfo);
return;
@@ -638,20 +656,21 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
String deviceApiVersionName = device.getProperty(IDevice.PROP_BUILD_VERSION);
String value = device.getProperty(IDevice.PROP_BUILD_VERSION_NUMBER);
int deviceApiVersionNumber = 0;
int deviceApiVersionNumber = AndroidManifestParser.INVALID_MIN_SDK;
try {
deviceApiVersionNumber = Integer.parseInt(value);
} catch (NumberFormatException e) {
// pass, we'll keep the deviceVersionNumber value at 0.
}
if (launchInfo.getRequiredApiVersionNumber() == 0) {
if (launchInfo.getRequiredApiVersionNumber() == AndroidManifestParser.INVALID_MIN_SDK) {
// warn the API level requirement is not set.
AdtPlugin.printErrorToConsole(launchInfo.getProject(),
"WARNING: Application does not specify an API level requirement!");
// and display the target device API level (if known)
if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
if (deviceApiVersionName == null ||
deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
AdtPlugin.printErrorToConsole(launchInfo.getProject(),
"WARNING: Unknown device API version!");
} else {
@@ -660,7 +679,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
deviceApiVersionName));
}
} else { // app requires a specific API level
if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
if (deviceApiVersionName == null ||
deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
AdtPlugin.printToConsole(launchInfo.getProject(),
"WARNING: Unknown device API version!");
} else if (deviceApiVersionNumber < launchInfo.getRequiredApiVersionNumber()) {
@@ -792,6 +812,14 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
return false;
}
// The app is now installed, now try the dependent projects
for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
String msg = String.format("Project dependency found, syncing: %s",
dependentLaunchInfo.getProject().getName());
AdtPlugin.printToConsole(launchInfo.getProject(), msg);
syncApp(dependentLaunchInfo, device);
}
return installResult;
}
@@ -803,6 +831,81 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
return false;
}
/**
* For the current launchInfo, create additional DelayedLaunchInfo that should be used to
* sync APKs that we are dependent on to the device.
*
* @param launchInfo the original launch info that we want to find the
* @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
*/
public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
// Convert to equivalent JavaProject
IJavaProject javaProject;
try {
//assuming this is an Android (and Java) project since it is attached to the launchInfo.
javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
} catch (CoreException e) {
// return empty dependencies
AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
return dependencies;
}
// Get all projects that this depends on
List<IJavaProject> androidProjectList;
try {
androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
} catch (JavaModelException e) {
// return empty dependencies
AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
return dependencies;
}
// for each project, parse manifest and create launch information
for (IJavaProject androidProject : androidProjectList) {
// Parse the Manifest to get various required information
// copied from LaunchConfigDelegate
AndroidManifestParser manifestParser;
try {
manifestParser = AndroidManifestParser.parse(
androidProject, null /* errorListener */,
true /* gatherData */, false /* markErrors */);
} catch (CoreException e) {
AdtPlugin.printErrorToConsole(
launchInfo.getProject(),
String.format("Error parsing manifest of %s",
androidProject.getElementName()));
continue;
}
// Get the APK location (can return null)
IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
if (apk == null) {
// getApplicationPackage will have logged an error message
continue;
}
// Create new launchInfo as an hybrid between parent and dependency information
DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
androidProject.getProject(),
manifestParser.getPackage(),
launchInfo.getLaunchAction(),
apk,
manifestParser.getDebuggable(),
manifestParser.getApiLevelRequirement(),
launchInfo.getLaunch(),
launchInfo.getMonitor());
// Add to the list
dependencies.add(delayedLaunchInfo);
}
return dependencies;
}
/**
* Installs the application package that was pushed to a temporary location on the device.
* @param launchInfo The launch information

View File

@@ -16,12 +16,13 @@
package com.android.ide.eclipse.adt.launch;
import com.android.ddmlib.IDevice;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
import com.android.ddmlib.IDevice;
/**
* A delayed launch waiting for a device to be present or ready before the
* application is launched.
@@ -50,7 +51,8 @@ public final class DelayedLaunchInfo {
/** debuggable attribute of the manifest file. */
private final Boolean mDebuggable;
/** Required ApiVersionNumber by the app. 0 means no requirements */
/** Required ApiVersionNumber by the app. {@link AndroidManifestParser#INVALID_MIN_SDK} means
* no requirements */
private final int mRequiredApiVersionNumber;
private InstallRetryMode mRetryMode = InstallRetryMode.NEVER;
@@ -81,7 +83,8 @@ public final class DelayedLaunchInfo {
* @param launchAction action to perform after app install
* @param pack IFile to the package (.apk) file
* @param debuggable debuggable attribute of the app's manifest file.
* @param requiredApiVersionNumber required SDK version by the app. 0 means no requirements.
* @param requiredApiVersionNumber required SDK version by the app.
* {@link AndroidManifestParser#INVALID_MIN_SDK} means no requirements.
* @param launch the launch object
* @param monitor progress monitor for launch
*/

View File

@@ -17,6 +17,7 @@
package com.android.ide.eclipse.adt.launch;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.ide.eclipse.ddms.DdmsPlugin;
@@ -292,14 +293,15 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
public void initializeFrom(ILaunchConfiguration configuration) {
AvdManager avdManager = Sdk.getCurrent().getAvdManager();
boolean value = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic
TargetMode mode = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic
try {
value = configuration.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, value);
mode = TargetMode.getMode(configuration.getAttribute(
LaunchConfigDelegate.ATTR_TARGET_MODE, mode.getValue()));
} catch (CoreException e) {
// let's not do anything here, we'll use the default value
}
mAutoTargetButton.setSelection(value);
mManualTargetButton.setSelection(!value);
mAutoTargetButton.setSelection(mode.getValue());
mManualTargetButton.setSelection(!mode.getValue());
// look for the project name to get its target.
String stringValue = "";
@@ -354,7 +356,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
mPreferredAvdSelector.setSelection(null);
}
value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
boolean value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
try {
value = configuration.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, value);
} catch (CoreException e) {
@@ -440,7 +442,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
*/
public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
LaunchConfigDelegate.DEFAULT_TARGET_MODE);
LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());
configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
LaunchConfigDelegate.DEFAULT_SPEED);
configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,

View File

@@ -18,15 +18,14 @@ package com.android.ide.eclipse.adt.launch;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
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.CoreException;
@@ -51,7 +50,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
/** Target mode parameters: true is automatic, false is manual */
public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
public static final boolean DEFAULT_TARGET_MODE = true; //automatic mode
public static final TargetMode DEFAULT_TARGET_MODE = TargetMode.AUTO;
/**
* Launch action:
@@ -152,7 +151,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
AdtPlugin.printToConsole(project, "Android Launch!");
// check if the project is using the proper sdk.
// if that throws an exception, we simply let it propage to the caller.
// if that throws an exception, we simply let it propagate to the caller.
if (checkAndroidProject(project) == false) {
AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
androidLaunch.stopLaunch();
@@ -215,7 +214,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
AndroidLaunchController controller = AndroidLaunchController.getInstance();
// get the application package
IFile applicationPackage = getApplicationPackage(project);
IFile applicationPackage = ProjectHelper.getApplicationPackage(project);
if (applicationPackage == null) {
androidLaunch.stopLaunch();
return;
@@ -387,39 +386,6 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
}
/**
* Returns the android package file as an IFile object for the specified
* project.
* @param project The project
* @return The android package as an IFile object or null if not found.
*/
private IFile getApplicationPackage(IProject project) {
// get the output folder
IFolder outputLocation = BaseProjectHelper.getOutputFolder(project);
if (outputLocation == null) {
AdtPlugin.printErrorToConsole(project,
"Failed to get the output location of the project. Check build path properties"
);
return null;
}
// get the package path
String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
IResource r = outputLocation.findMember(packageName);
// check the package is present
if (r instanceof IFile && r.exists()) {
return (IFile)r;
}
String msg = String.format("Could not find %1$s!", packageName);
AdtPlugin.printErrorToConsole(project, msg);
return null;
}
/**
* Returns the name of the activity.
*/

View File

@@ -55,6 +55,11 @@ import org.eclipse.swt.widgets.Text;
*/
public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
/**
*
*/
public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab.png"; //$NON-NLS-1$
protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
protected Text mProjText;
@@ -194,7 +199,7 @@ public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
@Override
public Image getImage() {
return AdtPlugin.getImageLoader().loadImage("mainLaunchTab.png", null);
return AdtPlugin.getImageLoader().loadImage(LAUNCH_TAB_IMAGE, null);
}
@@ -310,21 +315,8 @@ public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
}
mProjText.setText(projectName);
// get the list of projects
IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
if (projects != null) {
// look for the currently selected project
IProject proj = null;
for (IJavaProject p : projects) {
if (p.getElementName().equals(projectName)) {
proj = p.getProject();
break;
}
}
loadActivities(proj);
}
IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
loadActivities(proj);
// load the launch action.
mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;

View File

@@ -0,0 +1,276 @@
/*
* Copyright (C) 2009 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.launch.junit;
import com.android.ddmlib.IDevice;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo;
import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
import com.android.ide.eclipse.adt.launch.junit.runtime.AndroidJUnitLaunchInfo;
import com.android.ide.eclipse.adt.launch.junit.runtime.RemoteAdtTestRunner;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
import org.eclipse.jdt.launching.IVMRunner;
import org.eclipse.jdt.launching.VMRunnerConfiguration;
/**
* A launch action that executes a instrumentation test run on an Android device.
*/
class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
private String mTestPackage;
private String mRunner;
/**
* Creates a AndroidJUnitLaunchAction.
*
* @param testPackage the Android application package that contains the tests to run
* @param runner the InstrumentationTestRunner that will execute the tests
*/
public AndroidJUnitLaunchAction(String testPackage, String runner) {
mTestPackage = testPackage;
mRunner = runner;
}
/**
* Launch a instrumentation test run on given Android device.
* Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
*
* @see IAndroidLaunchAction#doLaunchAction(DelayedLaunchInfo, IDevice)
*/
public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
String msg = String.format("Launching instrumentation %s on device %s", mRunner,
device.getSerialNumber());
AdtPlugin.printToConsole(info.getProject(), msg);
try {
JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(info, device);
final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE :
ILaunchManager.RUN_MODE;
junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
info.getMonitor());
// TODO: need to add AMReceiver-type functionality somewhere
} catch (CoreException e) {
AdtPlugin.printErrorToConsole(info.getProject(), "Failed to launch test");
}
return true;
}
/**
* {@inheritDoc}
*/
public String getLaunchDescription() {
return String.format("%s JUnit launch", mRunner);
}
/**
* Extends the JDT JUnit launch delegate to allow for JUnit UI reuse.
*/
private class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
private IDevice mDevice;
private DelayedLaunchInfo mLaunchInfo;
public JUnitLaunchDelegate(DelayedLaunchInfo info, IDevice device) {
mLaunchInfo = info;
mDevice = device;
}
/* (non-Javadoc)
* @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public synchronized void launch(ILaunchConfiguration configuration, String mode,
ILaunch launch, IProgressMonitor monitor) throws CoreException {
// TODO: is progress monitor adjustment needed here?
super.launch(configuration, mode, launch, monitor);
}
/**
* {@inheritDoc}
* @throws CoreException
* @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
*/
@Override
public String verifyMainTypeName(ILaunchConfiguration configuration) throws CoreException {
return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
}
/**
* Overrides parent to return a VM Runner implementation which launches a thread, rather
* than a separate VM process
* @throws CoreException
*/
@Override
public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode)
throws CoreException {
return new VMTestRunner(new AndroidJUnitLaunchInfo(mLaunchInfo.getProject(),
mTestPackage, mRunner, mLaunchInfo.isDebugMode(), mDevice));
}
/**
* {@inheritDoc}
* @throws CoreException
* @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
*/
@Override
public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
throws CoreException {
return mLaunchInfo.getLaunch();
}
}
/**
* Provides a VM runner implementation which starts a thread implementation of a launch process
*/
private static class VMTestRunner implements IVMRunner {
private final AndroidJUnitLaunchInfo mJUnitInfo;
VMTestRunner(AndroidJUnitLaunchInfo info) {
mJUnitInfo = info;
}
/**
* {@inheritDoc}
* @throws CoreException
*/
public void run(final VMRunnerConfiguration config, ILaunch launch,
IProgressMonitor monitor) throws CoreException {
TestRunnerProcess runnerProcess =
new TestRunnerProcess(config, launch, mJUnitInfo);
runnerProcess.start();
launch.addProcess(runnerProcess);
}
}
/**
* Launch process that executes the tests.
*/
private static class TestRunnerProcess extends Thread implements IProcess {
private final VMRunnerConfiguration mRunConfig;
private final ILaunch mLaunch;
private final AndroidJUnitLaunchInfo mJUnitInfo;
private RemoteAdtTestRunner mTestRunner = null;
private boolean mIsTerminated = false;
TestRunnerProcess(VMRunnerConfiguration runConfig, ILaunch launch,
AndroidJUnitLaunchInfo info) {
mRunConfig = runConfig;
mLaunch = launch;
mJUnitInfo = info;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
*/
public String getAttribute(String key) {
return null;
}
/**
* {@inheritDoc}
* @throws DebugException
* @see org.eclipse.debug.core.model.IProcess#getExitValue()
*/
public int getExitValue() throws DebugException {
return 0;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getLabel()
*/
public String getLabel() {
return mLaunch.getLaunchMode();
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getLaunch()
*/
public ILaunch getLaunch() {
return mLaunch;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
*/
public IStreamsProxy getStreamsProxy() {
return null;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String,
* java.lang.String)
*/
public void setAttribute(String key, String value) {
// ignore
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
*/
@SuppressWarnings("unchecked")
public Object getAdapter(Class adapter) {
return null;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ITerminate#canTerminate()
*/
public boolean canTerminate() {
return true;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ITerminate#isTerminated()
*/
public boolean isTerminated() {
return mIsTerminated;
}
/**
* {@inheritDoc}
* @throws DebugException
* @see org.eclipse.debug.core.model.ITerminate#terminate()
*/
public void terminate() throws DebugException {
if (mTestRunner != null) {
mTestRunner.terminate();
}
mIsTerminated = true;
}
/**
* Launches a test runner that will communicate results back to JDT JUnit UI
*/
@Override
public void run() {
mTestRunner = new RemoteAdtTestRunner();
mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2009 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.launch.junit;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunch;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration;
import com.android.ide.eclipse.adt.launch.AndroidLaunchController;
import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
import com.android.ide.eclipse.adt.launch.LaunchConfigDelegate;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
/**
* Run configuration that can execute JUnit tests on an Android platform.
* <p/>
* Will deploy apps on target Android platform by reusing functionality from ADT
* LaunchConfigDelegate, and then run JUnits tests by reusing functionality from JDT
* JUnitLaunchConfigDelegate.
*/
@SuppressWarnings("restriction")
public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
/** Launch config attribute that stores instrumentation runner. */
static final String ATTR_INSTR_NAME = AdtPlugin.PLUGIN_ID + ".instrumentation"; //$NON-NLS-1$
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
@Override
protected void doLaunch(final ILaunchConfiguration configuration, final String mode,
IProgressMonitor monitor, IProject project, final AndroidLaunch androidLaunch,
AndroidLaunchConfiguration config, AndroidLaunchController controller,
IFile applicationPackage, AndroidManifestParser manifestParser) {
String testPackage = manifestParser.getPackage();
String runner = getRunner(project, configuration, manifestParser);
if (runner == null) {
AdtPlugin.displayError("Android Launch",
"An instrumention test runner is not specified!");
androidLaunch.stopLaunch();
return;
}
IAndroidLaunchAction junitLaunch = new AndroidJUnitLaunchAction(testPackage, runner);
controller.launch(project, mode, applicationPackage, manifestParser.getPackage(),
manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(),
junitLaunch, config, androidLaunch, monitor);
}
/**
* Gets a instrumentation runner for the launch.
* <p/>
* If a runner is stored in the given <code>configuration</code>, will return that.
* Otherwise, will try to find the first valid runner for the project.
* If a runner can still not be found, will return <code>null</code>.
*
* @param project the {@link IProject} for the app
* @param configuration the {@link ILaunchConfiguration} for the launch
* @param manifestParser the {@link AndroidManifestParser} for the project
*
* @return <code>null</code> if no instrumentation runner can be found, otherwise return
* the fully qualified runner name.
*/
private String getRunner(IProject project, ILaunchConfiguration configuration,
AndroidManifestParser manifestParser) {
try {
String runner = getRunnerFromConfig(configuration);
if (runner != null) {
return runner;
}
final InstrumentationRunnerValidator instrFinder = new InstrumentationRunnerValidator(
BaseProjectHelper.getJavaProject(project), manifestParser);
runner = instrFinder.getValidInstrumentationTestRunner();
if (runner != null) {
AdtPlugin.printErrorToConsole(project,
String.format("Warning: No instrumentation runner found for the launch, " +
"using %1$s", runner));
return runner;
}
AdtPlugin.printErrorToConsole(project,
String.format("ERROR: Application does not specify a %1$s instrumentation or does not declare uses-library %2$s",
AndroidConstants.CLASS_INSTRUMENTATION_RUNNER,
AndroidConstants.LIBRARY_TEST_RUNNER));
return null;
} catch (CoreException e) {
AdtPlugin.log(e, "Error when retrieving instrumentation info"); //$NON-NLS-1$
}
return null;
}
private String getRunnerFromConfig(ILaunchConfiguration configuration) throws CoreException {
String runner = configuration.getAttribute(ATTR_INSTR_NAME, EMPTY_STRING);
if (runner.length() < 1) {
return null;
}
return runner;
}
/**
* Helper method to set JUnit-related attributes expected by JDT JUnit runner
*
* @param config the launch configuration to modify
*/
static void setJUnitDefaults(ILaunchConfigurationWorkingCopy config) {
// set the test runner to JUnit3 to placate JDT JUnit runner logic
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
TestKindRegistry.JUNIT3_TEST_KIND_ID);
}
}

View File

@@ -0,0 +1,977 @@
/*
* Copyright (C) 2009 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.launch.junit;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.MainLaunchConfigTab;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.ProjectChooserHelper;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.junit.Messages;
import org.eclipse.jdt.internal.junit.launcher.ITestKind;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
import org.eclipse.jdt.internal.junit.launcher.JUnitMigrationDelegate;
import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
import org.eclipse.jdt.internal.junit.launcher.TestSelectionDialog;
import org.eclipse.jdt.internal.junit.ui.JUnitMessages;
import org.eclipse.jdt.internal.junit.util.LayoutUtil;
import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator;
import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.ui.JavaElementComparator;
import org.eclipse.jdt.ui.JavaElementLabelProvider;
import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
import org.eclipse.ui.dialogs.SelectionDialog;
import java.lang.reflect.InvocationTargetException;
/**
* The launch config UI tab for Android JUnit
* <p/>
* Based on org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationTab
*/
@SuppressWarnings("restriction")
public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab {
// Project UI widgets
private Label mProjLabel;
private Text mProjText;
private Button mProjButton;
// Test class UI widgets
private Text mTestText;
private Button mSearchButton;
private String mOriginalTestMethodName;
private Label mTestMethodLabel;
private Text mContainerText;
private IJavaElement mContainerElement;
private final ILabelProvider mJavaElementLabelProvider = new JavaElementLabelProvider();
private Button mContainerSearchButton;
private Button mTestContainerRadioButton;
private Button mTestRadioButton;
private Label mTestLabel;
// Android specific members
private Image mTabIcon = null;
private Combo mInstrumentationCombo;
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
private static final String TAG = "AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
private String[] mInstrumentations = null;
private InstrumentationRunnerValidator mInstrValidator = null;
private ProjectChooserHelper mProjectChooserHelper;
/* (non-Javadoc)
* @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
*/
public void createControl(Composite parent) {
mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
Composite comp = new Composite(parent, SWT.NONE);
setControl(comp);
GridLayout topLayout = new GridLayout();
topLayout.numColumns = 3;
comp.setLayout(topLayout);
createSingleTestSection(comp);
createTestContainerSelectionGroup(comp);
createSpacer(comp);
createInstrumentationGroup(comp);
createSpacer(comp);
Dialog.applyDialogFont(comp);
// TODO: add help link here when available
//PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(),
// IJUnitHelpContextIds.LAUNCH_CONFIGURATION_DIALOG_JUNIT_MAIN_TAB);
validatePage();
}
private void createSpacer(Composite comp) {
Label label = new Label(comp, SWT.NONE);
GridData gd = new GridData();
gd.horizontalSpan = 3;
label.setLayoutData(gd);
}
private void createSingleTestSection(Composite comp) {
mTestRadioButton = new Button(comp, SWT.RADIO);
mTestRadioButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_oneTest);
GridData gd = new GridData();
gd.horizontalSpan = 3;
mTestRadioButton.setLayoutData(gd);
mTestRadioButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (mTestRadioButton.getSelection()) {
testModeChanged();
}
}
});
mProjLabel = new Label(comp, SWT.NONE);
mProjLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_project);
gd = new GridData();
gd.horizontalIndent = 25;
mProjLabel.setLayoutData(gd);
mProjText = new Text(comp, SWT.SINGLE | SWT.BORDER);
mProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mProjText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent evt) {
validatePage();
updateLaunchConfigurationDialog();
mSearchButton.setEnabled(mTestRadioButton.getSelection() &&
mProjText.getText().length() > 0);
}
});
mProjButton = new Button(comp, SWT.PUSH);
mProjButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_browse);
mProjButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent evt) {
handleProjectButtonSelected();
}
});
setButtonGridData(mProjButton);
mTestLabel = new Label(comp, SWT.NONE);
gd = new GridData();
gd.horizontalIndent = 25;
mTestLabel.setLayoutData(gd);
mTestLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_test);
mTestText = new Text(comp, SWT.SINGLE | SWT.BORDER);
mTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mTestText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent evt) {
validatePage();
updateLaunchConfigurationDialog();
}
});
mSearchButton = new Button(comp, SWT.PUSH);
mSearchButton.setEnabled(mProjText.getText().length() > 0);
mSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
mSearchButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent evt) {
handleSearchButtonSelected();
}
});
setButtonGridData(mSearchButton);
new Label(comp, SWT.NONE);
mTestMethodLabel = new Label(comp, SWT.NONE);
mTestMethodLabel.setText(""); //$NON-NLS-1$
gd = new GridData();
gd.horizontalSpan = 2;
mTestMethodLabel.setLayoutData(gd);
}
private void createTestContainerSelectionGroup(Composite comp) {
mTestContainerRadioButton = new Button(comp, SWT.RADIO);
mTestContainerRadioButton.setText(
JUnitMessages.JUnitLaunchConfigurationTab_label_containerTest);
GridData gd = new GridData();
gd.horizontalSpan = 3;
mTestContainerRadioButton.setLayoutData(gd);
mTestContainerRadioButton.addSelectionListener(new SelectionListener() {
public void widgetSelected(SelectionEvent e) {
if (mTestContainerRadioButton.getSelection()) {
testModeChanged();
}
}
public void widgetDefaultSelected(SelectionEvent e) {
}
});
mContainerText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.READ_ONLY);
gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalIndent = 25;
gd.horizontalSpan = 2;
mContainerText.setLayoutData(gd);
mContainerText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent evt) {
updateLaunchConfigurationDialog();
}
});
mContainerSearchButton = new Button(comp, SWT.PUSH);
mContainerSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
mContainerSearchButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent evt) {
handleContainerSearchButtonSelected();
}
});
setButtonGridData(mContainerSearchButton);
}
private void createInstrumentationGroup(Composite comp) {
Label loaderLabel = new Label(comp, SWT.NONE);
loaderLabel.setText("Instrumentation runner:");
GridData gd = new GridData();
gd.horizontalIndent = 0;
loaderLabel.setLayoutData(gd);
mInstrumentationCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
gd = new GridData(GridData.FILL_HORIZONTAL);
mInstrumentationCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mInstrumentationCombo.clearSelection();
mInstrumentationCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
validatePage();
updateLaunchConfigurationDialog();
}
});
}
private void handleContainerSearchButtonSelected() {
IJavaElement javaElement = chooseContainer(mContainerElement);
if (javaElement != null) {
setContainerElement(javaElement);
}
}
private void setContainerElement(IJavaElement javaElement) {
mContainerElement = javaElement;
mContainerText.setText(getPresentationName(javaElement));
validatePage();
updateLaunchConfigurationDialog();
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
*/
public void initializeFrom(ILaunchConfiguration config) {
String projectName = updateProjectFromConfig(config);
String containerHandle = EMPTY_STRING;
try {
containerHandle = config.getAttribute(
JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
} catch (CoreException ce) {
// ignore
}
if (containerHandle.length() > 0) {
updateTestContainerFromConfig(config);
} else {
updateTestTypeFromConfig(config);
}
IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
loadInstrumentations(proj);
updateInstrumentationFromConfig(config);
validatePage();
}
private void updateInstrumentationFromConfig(ILaunchConfiguration config) {
boolean found = false;
try {
String currentInstrumentation = config.getAttribute(
AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, EMPTY_STRING);
if (mInstrumentations != null) {
// look for the name of the instrumentation in the combo.
for (int i = 0; i < mInstrumentations.length; i++) {
if (currentInstrumentation.equals(mInstrumentations[i])) {
found = true;
mInstrumentationCombo.select(i);
break;
}
}
}
} catch (CoreException ce) {
// ignore
}
if (!found) {
mInstrumentationCombo.clearSelection();
}
}
private String updateProjectFromConfig(ILaunchConfiguration config) {
String projectName = EMPTY_STRING;
try {
projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
EMPTY_STRING);
} catch (CoreException ce) {
// ignore
}
mProjText.setText(projectName);
return projectName;
}
private void updateTestTypeFromConfig(ILaunchConfiguration config) {
String testTypeName = EMPTY_STRING;
mOriginalTestMethodName = EMPTY_STRING;
try {
testTypeName = config.getAttribute(
IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
mOriginalTestMethodName = config.getAttribute(
JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
} catch (CoreException ce) {
// ignore
}
mTestRadioButton.setSelection(true);
setEnableSingleTestGroup(true);
setEnableContainerTestGroup(false);
mTestContainerRadioButton.setSelection(false);
mTestText.setText(testTypeName);
mContainerText.setText(EMPTY_STRING);
setTestMethodLabel(mOriginalTestMethodName);
}
private void setTestMethodLabel(String testMethodName) {
if (!EMPTY_STRING.equals(testMethodName)) {
mTestMethodLabel.setText(
JUnitMessages.JUnitLaunchConfigurationTab_label_method +
mOriginalTestMethodName);
} else {
mTestMethodLabel.setText(EMPTY_STRING);
}
}
private void updateTestContainerFromConfig(ILaunchConfiguration config) {
String containerHandle = EMPTY_STRING;
IJavaElement containerElement = null;
try {
containerHandle = config.getAttribute(
JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
if (containerHandle.length() > 0) {
containerElement = JavaCore.create(containerHandle);
}
} catch (CoreException ce) {
// ignore
}
if (containerElement != null) {
mContainerElement = containerElement;
}
mTestContainerRadioButton.setSelection(true);
setEnableSingleTestGroup(false);
setEnableContainerTestGroup(true);
mTestRadioButton.setSelection(false);
if (mContainerElement != null) {
mContainerText.setText(getPresentationName(mContainerElement));
}
mTestText.setText(EMPTY_STRING);
}
/*
* (non-Javadoc)
* @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
*/
public void performApply(ILaunchConfigurationWorkingCopy config) {
if (mTestContainerRadioButton.getSelection() && mContainerElement != null) {
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
mContainerElement.getJavaProject().getElementName());
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
mContainerElement.getHandleIdentifier());
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
EMPTY_STRING);
//workaround for Eclipse bug 65399
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
EMPTY_STRING);
} else {
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
mProjText.getText());
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
mTestText.getText());
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
EMPTY_STRING);
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
mOriginalTestMethodName);
}
try {
mapResources(config);
} catch (CoreException e) {
// TODO: does the real error need to be extracted out of CoreException
AdtPlugin.log(e, "Error occurred saving configuration"); //$NON-NLS-1$
}
AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
getSelectedInstrumentation());
}
private void mapResources(ILaunchConfigurationWorkingCopy config) throws CoreException {
JUnitMigrationDelegate.mapResources(config);
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#dispose()
*/
@Override
public void dispose() {
super.dispose();
if (mTabIcon != null) {
mTabIcon.dispose();
mTabIcon = null;
}
mJavaElementLabelProvider.dispose();
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getImage()
*/
@Override
public Image getImage() {
// reuse icon from the Android App Launch config tab
if (mTabIcon == null) {
mTabIcon = AdtPlugin.getImageLoader().loadImage(MainLaunchConfigTab.LAUNCH_TAB_IMAGE,
null);
}
return mTabIcon;
}
/**
* Show a dialog that lists all main types
*/
private void handleSearchButtonSelected() {
Shell shell = getShell();
IJavaProject javaProject = getJavaProject();
IType[] types = new IType[0];
boolean[] radioSetting = new boolean[2];
try {
// fix for Eclipse bug 66922 Wrong radio behaviour when switching
// remember the selected radio button
radioSetting[0] = mTestRadioButton.getSelection();
radioSetting[1] = mTestContainerRadioButton.getSelection();
types = TestSearchEngine.findTests(getLaunchConfigurationDialog(), javaProject,
getTestKind());
} catch (InterruptedException e) {
setErrorMessage(e.getMessage());
return;
} catch (InvocationTargetException e) {
AdtPlugin.log(e.getTargetException(), "Error finding test types"); //$NON-NLS-1$
return;
} finally {
mTestRadioButton.setSelection(radioSetting[0]);
mTestContainerRadioButton.setSelection(radioSetting[1]);
}
SelectionDialog dialog = new TestSelectionDialog(shell, types);
dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_title);
dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_message);
if (dialog.open() == Window.CANCEL) {
return;
}
Object[] results = dialog.getResult();
if ((results == null) || (results.length < 1)) {
return;
}
IType type = (IType) results[0];
if (type != null) {
mTestText.setText(type.getFullyQualifiedName('.'));
javaProject = type.getJavaProject();
mProjText.setText(javaProject.getElementName());
}
}
private ITestKind getTestKind() {
// harddcode this to JUnit 3
return TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID);
}
/**
* Show a dialog that lets the user select a Android project. This in turn provides
* context for the main type, allowing the user to key a main type name, or
* constraining the search for main types to the specified project.
*/
private void handleProjectButtonSelected() {
IJavaProject project = mProjectChooserHelper.chooseJavaProject(getProjectName());
if (project == null) {
return;
}
String projectName = project.getElementName();
mProjText.setText(projectName);
loadInstrumentations(project.getProject());
}
/**
* Return the IJavaProject corresponding to the project name in the project name
* text field, or null if the text does not match a Android project name.
*/
private IJavaProject getJavaProject() {
String projectName = getProjectName();
return getJavaModel().getJavaProject(projectName);
}
/**
* Returns the name of the currently specified project. Null if no project is selected.
*/
private String getProjectName() {
String projectName = mProjText.getText().trim();
if (projectName.length() < 1) {
return null;
}
return projectName;
}
/**
* Convenience method to get the workspace root.
*/
private IWorkspaceRoot getWorkspaceRoot() {
return ResourcesPlugin.getWorkspace().getRoot();
}
/**
* Convenience method to get access to the java model.
*/
private IJavaModel getJavaModel() {
return JavaCore.create(getWorkspaceRoot());
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#isValid(org.eclipse.debug.core.ILaunchConfiguration)
*/
@Override
public boolean isValid(ILaunchConfiguration config) {
validatePage();
return getErrorMessage() == null;
}
private void testModeChanged() {
boolean isSingleTestMode = mTestRadioButton.getSelection();
setEnableSingleTestGroup(isSingleTestMode);
setEnableContainerTestGroup(!isSingleTestMode);
if (!isSingleTestMode && mContainerText.getText().length() == 0) {
String projText = mProjText.getText();
if (Path.EMPTY.isValidSegment(projText)) {
IJavaProject javaProject = getJavaModel().getJavaProject(projText);
if (javaProject != null && javaProject.exists()) {
setContainerElement(javaProject);
}
}
}
validatePage();
updateLaunchConfigurationDialog();
}
private void validatePage() {
setErrorMessage(null);
setMessage(null);
if (mTestContainerRadioButton.getSelection()) {
if (mContainerElement == null) {
setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_noContainer);
return;
}
validateJavaProject(mContainerElement.getJavaProject());
return;
}
String projectName = mProjText.getText().trim();
if (projectName.length() == 0) {
setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotdefined);
return;
}
IStatus status = ResourcesPlugin.getWorkspace().validatePath(IPath.SEPARATOR + projectName,
IResource.PROJECT);
if (!status.isOK() || !Path.ROOT.isValidSegment(projectName)) {
setErrorMessage(Messages.format(
JUnitMessages.JUnitLaunchConfigurationTab_error_invalidProjectName,
projectName));
return;
}
IProject project = getWorkspaceRoot().getProject(projectName);
if (!project.exists()) {
setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotexists);
return;
}
IJavaProject javaProject = JavaCore.create(project);
validateJavaProject(javaProject);
try {
if (!project.hasNature(AndroidConstants.NATURE)) {
setErrorMessage("Specified project is not an Android project");
return;
}
String className = mTestText.getText().trim();
if (className.length() == 0) {
setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
return;
}
if (javaProject.findType(className) == null) {
setErrorMessage(Messages.format(
JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found,
new String[] { className, projectName }));
return;
}
} catch (CoreException e) {
AdtPlugin.log(e, "validatePage failed"); //$NON-NLS-1$
}
validateInstrumentation();
}
private void validateJavaProject(IJavaProject javaProject) {
if (!TestSearchEngine.hasTestCaseType(javaProject)) {
setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testcasenotonpath);
return;
}
}
private void validateInstrumentation() {
String instrumentation = getSelectedInstrumentation();
if (instrumentation == null) {
setErrorMessage("Instrumentation runner not specified");
return;
}
String result = mInstrValidator.validateInstrumentationRunner(instrumentation);
if (result != InstrumentationRunnerValidator.INSTRUMENTATION_OK) {
setErrorMessage(result);
return;
}
}
private String getSelectedInstrumentation() {
int selectionIndex = mInstrumentationCombo.getSelectionIndex();
if (mInstrumentations != null && selectionIndex >= 0 &&
selectionIndex < mInstrumentations.length) {
return mInstrumentations[selectionIndex];
}
return null;
}
private void setEnableContainerTestGroup(boolean enabled) {
mContainerSearchButton.setEnabled(enabled);
mContainerText.setEnabled(enabled);
}
private void setEnableSingleTestGroup(boolean enabled) {
mProjLabel.setEnabled(enabled);
mProjText.setEnabled(enabled);
mProjButton.setEnabled(enabled);
mTestLabel.setEnabled(enabled);
mTestText.setEnabled(enabled);
mSearchButton.setEnabled(enabled && mProjText.getText().length() > 0);
mTestMethodLabel.setEnabled(enabled);
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
*/
public void setDefaults(ILaunchConfigurationWorkingCopy config) {
IJavaElement javaElement = getContext();
if (javaElement != null) {
initializeJavaProject(javaElement, config);
} else {
// We set empty attributes for project & main type so that when one config is
// compared to another, the existence of empty attributes doesn't cause an
// incorrect result (the performApply() method can result in empty values
// for these attributes being set on a config if there is nothing in the
// corresponding text boxes)
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, EMPTY_STRING);
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
EMPTY_STRING);
}
initializeTestAttributes(javaElement, config);
}
private void initializeTestAttributes(IJavaElement javaElement,
ILaunchConfigurationWorkingCopy config) {
if (javaElement != null && javaElement.getElementType() < IJavaElement.COMPILATION_UNIT) {
initializeTestContainer(javaElement, config);
} else {
initializeTestType(javaElement, config);
}
}
private void initializeTestContainer(IJavaElement javaElement,
ILaunchConfigurationWorkingCopy config) {
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
javaElement.getHandleIdentifier());
initializeName(config, javaElement.getElementName());
}
private void initializeName(ILaunchConfigurationWorkingCopy config, String name) {
if (name == null) {
name = EMPTY_STRING;
}
if (name.length() > 0) {
int index = name.lastIndexOf('.');
if (index > 0) {
name = name.substring(index + 1);
}
name = getLaunchConfigurationDialog().generateName(name);
config.rename(name);
}
}
/**
* Sets the main type & name attributes on the working copy based on the IJavaElement
*/
private void initializeTestType(IJavaElement javaElement,
ILaunchConfigurationWorkingCopy config) {
String name = EMPTY_STRING;
String testKindId = null;
try {
// only do a search for compilation units or class files or source references
if (javaElement instanceof ISourceReference) {
ITestKind testKind = TestKindRegistry.getContainerTestKind(javaElement);
testKindId = testKind.getId();
IType[] types = TestSearchEngine.findTests(getLaunchConfigurationDialog(),
javaElement, testKind);
if ((types == null) || (types.length < 1)) {
return;
}
// Simply grab the first main type found in the searched element
name = types[0].getFullyQualifiedName('.');
}
} catch (InterruptedException ie) {
// ignore
} catch (InvocationTargetException ite) {
// ignore
}
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, name);
if (testKindId != null) {
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
testKindId);
}
initializeName(config, name);
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
*/
public String getName() {
return JUnitMessages.JUnitLaunchConfigurationTab_tab_label;
}
@SuppressWarnings("unchecked")
private IJavaElement chooseContainer(IJavaElement initElement) {
Class[] acceptedClasses = new Class[] { IPackageFragmentRoot.class, IJavaProject.class,
IPackageFragment.class };
TypedElementSelectionValidator validator = new TypedElementSelectionValidator(
acceptedClasses, false) {
@Override
public boolean isSelectedValid(Object element) {
return true;
}
};
acceptedClasses = new Class[] { IJavaModel.class, IPackageFragmentRoot.class,
IJavaProject.class, IPackageFragment.class };
ViewerFilter filter = new TypedViewerFilter(acceptedClasses) {
@Override
public boolean select(Viewer viewer, Object parent, Object element) {
if (element instanceof IPackageFragmentRoot &&
((IPackageFragmentRoot) element).isArchive()) {
return false;
}
try {
if (element instanceof IPackageFragment &&
!((IPackageFragment) element).hasChildren()) {
return false;
}
} catch (JavaModelException e) {
return false;
}
return super.select(viewer, parent, element);
}
};
StandardJavaElementContentProvider provider = new StandardJavaElementContentProvider();
ILabelProvider labelProvider = new JavaElementLabelProvider(
JavaElementLabelProvider.SHOW_DEFAULT);
ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(),
labelProvider, provider);
dialog.setValidator(validator);
dialog.setComparator(new JavaElementComparator());
dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_title);
dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_message);
dialog.addFilter(filter);
dialog.setInput(JavaCore.create(getWorkspaceRoot()));
dialog.setInitialSelection(initElement);
dialog.setAllowMultiple(false);
if (dialog.open() == Window.OK) {
Object element = dialog.getFirstResult();
return (IJavaElement) element;
}
return null;
}
private String getPresentationName(IJavaElement element) {
return mJavaElementLabelProvider.getText(element);
}
/**
* Returns the current Java element context from which to initialize
* default settings, or <code>null</code> if none.
*
* @return Java element context.
*/
private IJavaElement getContext() {
IWorkbenchWindow activeWorkbenchWindow =
PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (activeWorkbenchWindow == null) {
return null;
}
IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
if (page != null) {
ISelection selection = page.getSelection();
if (selection instanceof IStructuredSelection) {
IStructuredSelection ss = (IStructuredSelection) selection;
if (!ss.isEmpty()) {
Object obj = ss.getFirstElement();
if (obj instanceof IJavaElement) {
return (IJavaElement) obj;
}
if (obj instanceof IResource) {
IJavaElement je = JavaCore.create((IResource) obj);
if (je == null) {
IProject pro = ((IResource) obj).getProject();
je = JavaCore.create(pro);
}
if (je != null) {
return je;
}
}
}
}
IEditorPart part = page.getActiveEditor();
if (part != null) {
IEditorInput input = part.getEditorInput();
return (IJavaElement) input.getAdapter(IJavaElement.class);
}
}
return null;
}
private void initializeJavaProject(IJavaElement javaElement,
ILaunchConfigurationWorkingCopy config) {
IJavaProject javaProject = javaElement.getJavaProject();
String name = null;
if (javaProject != null && javaProject.exists()) {
name = javaProject.getElementName();
}
config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
}
private void setButtonGridData(Button button) {
GridData gridData = new GridData();
button.setLayoutData(gridData);
LayoutUtil.setButtonDimensionHint(button);
}
/* (non-Javadoc)
* @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getId()
*/
@Override
public String getId() {
return "com.android.ide.eclipse.adt.launch.AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
}
/**
* Loads the UI with the instrumentations of the specified project, and stores the
* instrumentations in <code>mInstrumentations</code>.
*
* @param project the {@link IProject} to load the instrumentations from.
*/
private void loadInstrumentations(IProject project) {
try {
mInstrValidator = new InstrumentationRunnerValidator(project);
mInstrumentations = (mInstrValidator == null ? null :
mInstrValidator.getInstrumentations());
if (mInstrumentations != null) {
mInstrumentationCombo.removeAll();
for (String instrumentation : mInstrumentations) {
mInstrumentationCombo.add(instrumentation);
}
// the selection will be set when we update the ui from the current
// config object.
return;
}
} catch (CoreException e) {
AdtPlugin.logAndPrintError(e, TAG, "ERROR: Failed to get instrumentations for %1$s",
project.getName());
}
// if we reach this point, either project is null, or we got an exception during
// the parsing. In either case, we empty the instrumentation list.
mInstrValidator = null;
mInstrumentations = null;
mInstrumentationCombo.removeAll();
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2009 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.launch.junit;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
/**
* Launch shortcut to launch debug/run Android JUnit configuration directly.
*/
public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
@Override
protected String getLaunchConfigurationTypeId() {
return "com.android.ide.eclipse.adt.junit.launchConfigurationType"; //$NON-NLS-1$
}
/**
* Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
* first instrumentation found in the AndroidManifest.
*/
@Override
protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
throws CoreException {
ILaunchConfigurationWorkingCopy config = super.createLaunchConfiguration(element);
// just get first valid instrumentation runner
String instrumentation = new InstrumentationRunnerValidator(element.getJavaProject()).
getValidInstrumentationTestRunner();
if (instrumentation != null) {
config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
instrumentation);
}
// if a valid runner is not found, rely on launch delegate to log error.
// This method is called without explicit user action to launch Android JUnit, so avoid
// logging an error here.
AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
return config;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2009 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.launch.junit;
import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
import org.eclipse.debug.ui.CommonTab;
import org.eclipse.debug.ui.ILaunchConfigurationDialog;
import org.eclipse.debug.ui.ILaunchConfigurationTab;
import com.android.ide.eclipse.adt.launch.EmulatorConfigTab;
/**
* Tab group object for Android JUnit launch configuration type.
*/
public class AndroidJUnitTabGroup extends AbstractLaunchConfigurationTabGroup {
/**
* Creates the UI tabs for the Android JUnit configuration
*/
public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
new AndroidJUnitLaunchConfigurationTab(),
new EmulatorConfigTab(),
new CommonTab()
};
setTabs(tabs);
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2009 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.launch.junit;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaProject;
/**
* Provides validation for Android instrumentation test runner
*/
class InstrumentationRunnerValidator {
private final IJavaProject mJavaProject;
private String[] mInstrumentations = null;
private boolean mHasRunnerLibrary = false;
static final String INSTRUMENTATION_OK = null;
/**
* Initializes the InstrumentationRunnerValidator.
*
* @param javaProject the {@link IJavaProject} for the Android project to validate
*/
InstrumentationRunnerValidator(IJavaProject javaProject) {
mJavaProject = javaProject;
try {
AndroidManifestParser manifestParser = AndroidManifestParser.parse(javaProject,
null /* errorListener */, true /* gatherData */, false /* markErrors */);
init(manifestParser);
} catch (CoreException e) {
AdtPlugin.printErrorToConsole(javaProject.getProject(), "ERROR: Failed to parse %1$s",
AndroidConstants.FN_ANDROID_MANIFEST);
}
}
/**
* Initializes the InstrumentationRunnerValidator.
*
* @param project the {@link IProject} for the Android project to validate
* @throws CoreException if a fatal error occurred in initialization
*/
InstrumentationRunnerValidator(IProject project) throws CoreException {
this(BaseProjectHelper.getJavaProject(project));
}
/**
* Initializes the InstrumentationRunnerValidator with an existing {@link AndroidManifestParser}
*
* @param javaProject the {@link IJavaProject} for the Android project to validate
* @param manifestParser the {@link AndroidManifestParser} for the Android project
*/
InstrumentationRunnerValidator(IJavaProject javaProject, AndroidManifestParser manifestParser) {
mJavaProject = javaProject;
init(manifestParser);
}
private void init(AndroidManifestParser manifestParser) {
mInstrumentations = manifestParser.getInstrumentations();
mHasRunnerLibrary = hasTestRunnerLibrary(manifestParser);
}
/**
* Helper method to determine if given manifest has a <code>AndroidConstants.LIBRARY_TEST_RUNNER
* </code> library reference
*
* @param manifestParser the {@link AndroidManifestParser} to search
* @return true if test runner library found, false otherwise
*/
private boolean hasTestRunnerLibrary(AndroidManifestParser manifestParser) {
for (String lib : manifestParser.getUsesLibraries()) {
if (lib.equals(AndroidConstants.LIBRARY_TEST_RUNNER)) {
return true;
}
}
return false;
}
/**
* Return the set of instrumentations for the Android project.
*
* @return <code>null</code if error occurred parsing instrumentations, otherwise returns array
* of instrumentation class names
*/
String[] getInstrumentations() {
return mInstrumentations;
}
/**
* Helper method to get the first instrumentation that can be used as a test runner.
*
* @return fully qualified instrumentation class name. <code>null</code> if no valid
* instrumentation can be found.
*/
String getValidInstrumentationTestRunner() {
for (String instrumentation : getInstrumentations()) {
if (validateInstrumentationRunner(instrumentation) == INSTRUMENTATION_OK) {
return instrumentation;
}
}
return null;
}
/**
* Helper method to determine if specified instrumentation can be used as a test runner
*
* @param instrumentation the instrumentation class name to validate. Assumes this
* instrumentation is one of {@link #getInstrumentations()}
* @return <code>INSTRUMENTATION_OK</code> if valid, otherwise returns error message
*/
String validateInstrumentationRunner(String instrumentation) {
if (!mHasRunnerLibrary) {
return String.format("The application does not declare uses-library %1$s",
AndroidConstants.LIBRARY_TEST_RUNNER);
}
// check if this instrumentation is the standard test runner
if (!instrumentation.equals(AndroidConstants.CLASS_INSTRUMENTATION_RUNNER)) {
// check if it extends the standard test runner
String result = BaseProjectHelper.testClassForManifest(mJavaProject,
instrumentation, AndroidConstants.CLASS_INSTRUMENTATION_RUNNER, true);
if (result != BaseProjectHelper.TEST_CLASS_OK) {
return String.format("The instrumentation runner must be of type %s",
AndroidConstants.CLASS_INSTRUMENTATION_RUNNER);
}
}
return INSTRUMENTATION_OK;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import org.eclipse.core.resources.IProject;
import com.android.ddmlib.IDevice;
/**
* Contains info about Android JUnit launch
*/
public class AndroidJUnitLaunchInfo {
private final IProject mProject;
private final String mTestPackage;
private final String mRunner;
private final boolean mDebugMode;
private final IDevice mDevice;
public AndroidJUnitLaunchInfo(IProject project, String testPackage, String runner,
boolean debugMode, IDevice device) {
mProject = project;
mTestPackage = testPackage;
mRunner = runner;
mDebugMode = debugMode;
mDevice = device;
}
public IProject getProject() {
return mProject;
}
public String getTestPackage() {
return mTestPackage;
}
public String getRunner() {
return mRunner;
}
public boolean isDebugMode() {
return mDebugMode;
}
public IDevice getDevice() {
return mDevice;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import org.eclipse.jdt.internal.junit.runner.ITestIdentifier;
import org.eclipse.jdt.internal.junit.runner.ITestReference;
import org.eclipse.jdt.internal.junit.runner.TestExecution;
/**
* Base implementation of the Eclipse {@link ITestReference} and {@link ITestIdentifier} interfaces
* for Android tests.
* <p/>
* Provides generic equality/hashcode services
*/
@SuppressWarnings("restriction") //$NON-NLS-1$
abstract class AndroidTestReference implements ITestReference, ITestIdentifier {
/**
* Gets the {@link ITestIdentifier} for this test reference.
*/
public ITestIdentifier getIdentifier() {
// this class serves as its own test identifier
return this;
}
/**
* Not supported.
*/
public void run(TestExecution execution) {
throw new UnsupportedOperationException();
}
/**
* Compares {@link ITestIdentifier} using names
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof ITestIdentifier) {
ITestIdentifier testid = (ITestIdentifier) obj;
return getName().equals(testid.getName());
}
return false;
}
@Override
public int hashCode() {
return getName().hashCode();
}
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.ide.eclipse.adt.AdtPlugin;
import org.eclipse.jdt.internal.junit.runner.MessageIds;
import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
import org.eclipse.jdt.internal.junit.runner.TestExecution;
import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
/**
* Supports Eclipse JUnit execution of Android tests.
* <p/>
* Communicates back to a Eclipse JDT JUnit client via a socket connection.
*
* @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
*/
@SuppressWarnings("restriction")
public class RemoteAdtTestRunner extends RemoteTestRunner {
private AndroidJUnitLaunchInfo mLaunchInfo;
private TestExecution mExecution;
/**
* Initialize the JDT JUnit test runner parameters from the {@code args}.
*
* @param args name-value pair of arguments to pass to parent JUnit runner.
* @param launchInfo the Android specific test launch info
*/
protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) {
defaultInit(args);
mLaunchInfo = launchInfo;
}
/**
* Runs a set of tests, and reports back results using parent class.
* <p/>
* JDT Unit expects to be sent data in the following sequence:
* <ol>
* <li>The total number of tests to be executed.</li>
* <li>The test 'tree' data about the tests to be executed, which is composed of the set of
* test class names, the number of tests in each class, and the names of each test in the
* class.</li>
* <li>The test execution result for each test method. Expects individual notifications of
* the test execution start, any failures, and the end of the test execution.</li>
* <li>The end of the test run, with its elapsed time.</li>
* </ol>
* <p/>
* In order to satisfy this, this method performs two actual Android instrumentation runs.
* The first is a 'log only' run that will collect the test tree data, without actually
* executing the tests, and send it back to JDT JUnit. The second is the actual test execution,
* whose results will be communicated back in real-time to JDT JUnit.
*
* @param testClassNames array of fully qualified test class names to execute. Cannot be empty.
* @param testName test to execute. If null, will be ignored.
* @param execution used to report test progress
*/
@Override
public void runTests(String[] testClassNames, String testName, TestExecution execution) {
// hold onto this execution reference so it can be used to report test progress
mExecution = execution;
RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mLaunchInfo.getTestPackage(),
mLaunchInfo.getRunner(), mLaunchInfo.getDevice());
if (testClassNames != null && testClassNames.length > 0) {
if (testName != null) {
runner.setMethodName(testClassNames[0], testName);
} else {
runner.setClassNames(testClassNames);
}
}
// set log only to first collect test case info, so Eclipse has correct test case count/
// tree info
runner.setLogOnly(true);
TestCollector collector = new TestCollector();
runner.run(collector);
if (collector.getErrorMessage() != null) {
// error occurred during test collection.
reportError(collector.getErrorMessage());
// abort here
notifyTestRunEnded(0);
return;
}
notifyTestRunStarted(collector.getTestCaseCount());
collector.sendTrees(this);
// now do real execution
runner.setLogOnly(false);
if (mLaunchInfo.isDebugMode()) {
runner.setDebug(true);
}
runner.run(new TestRunListener());
}
/**
* Main entry method to run tests
*
* @param programArgs JDT JUnit program arguments to be processed by parent
* @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru
*/
public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
init(programArgs, junitInfo);
run();
}
/**
* Stop the current test run.
*/
public void terminate() {
stop();
}
@Override
protected void stop() {
if (mExecution != null) {
mExecution.stop();
}
}
private void notifyTestRunEnded(long elapsedTime) {
// copy from parent - not ideal, but method is private
sendMessage(MessageIds.TEST_RUN_END + elapsedTime);
flush();
//shutDown();
}
/**
* @param errorMessage
*/
private void reportError(String errorMessage) {
AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
String.format("Test run failed: %s", errorMessage));
// is this needed?
//notifyTestRunStopped(-1);
}
/**
* TestRunListener that communicates results in real-time back to JDT JUnit
*/
private class TestRunListener implements ITestRunListener {
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
*/
public void testEnded(TestIdentifier test) {
mExecution.getListener().notifyTestEnded(new TestCaseReference(test));
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
*/
public void testFailed(TestFailure status, TestIdentifier test, String trace) {
String statusString;
if (status == TestFailure.ERROR) {
statusString = MessageIds.TEST_ERROR;
} else {
statusString = MessageIds.TEST_FAILED;
}
TestReferenceFailure failure =
new TestReferenceFailure(new TestCaseReference(test),
statusString, trace, null);
mExecution.getListener().notifyTestFailed(failure);
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
*/
public void testRunEnded(long elapsedTime) {
notifyTestRunEnded(elapsedTime);
AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run complete");
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
*/
public void testRunFailed(String errorMessage) {
reportError(errorMessage);
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
*/
public void testRunStarted(int testCount) {
// ignore
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
*/
public void testRunStopped(long elapsedTime) {
notifyTestRunStopped(elapsedTime);
AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run stopped");
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
*/
public void testStarted(TestIdentifier test) {
TestCaseReference testId = new TestCaseReference(test);
mExecution.getListener().notifyTestStarted(testId);
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import com.android.ddmlib.testrunner.TestIdentifier;
import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
import org.eclipse.jdt.internal.junit.runner.MessageIds;
import java.text.MessageFormat;
/**
* Reference for a single Android test method.
*/
@SuppressWarnings("restriction")
class TestCaseReference extends AndroidTestReference {
private final String mClassName;
private final String mTestName;
/**
* Creates a TestCaseReference from a class and method name
*/
TestCaseReference(String className, String testName) {
mClassName = className;
mTestName = testName;
}
/**
* Creates a TestCaseReference from a {@link TestIdentifier}
* @param test
*/
TestCaseReference(TestIdentifier test) {
mClassName = test.getClassName();
mTestName = test.getTestName();
}
/**
* Returns a count of the number of test cases referenced. Is always one for this class.
*/
public int countTestCases() {
return 1;
}
/**
* Sends test identifier and test count information for this test
*
* @param notified the {@link IVisitsTestTrees} to send test info to
*/
public void sendTree(IVisitsTestTrees notified) {
notified.visitTreeEntry(getIdentifier(), false, countTestCases());
}
/**
* Returns the identifier of this test, in a format expected by JDT JUnit
*/
public String getName() {
return MessageFormat.format(MessageIds.TEST_IDENTIFIER_MESSAGE_FORMAT,
new Object[] { mTestName, mClassName});
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.TestIdentifier;
import org.eclipse.jdt.internal.junit.runner.ITestReference;
import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
import java.util.HashMap;
import java.util.Map;
/**
* Collects info about tests to be executed by listening to the results of an Android test run.
*/
@SuppressWarnings("restriction")
class TestCollector implements ITestRunListener {
private int mTotalTestCount;
/** test name to test suite reference map. */
private Map<String, TestSuiteReference> mTestTree;
private String mErrorMessage = null;
TestCollector() {
mTotalTestCount = 0;
mTestTree = new HashMap<String, TestSuiteReference>();
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
*/
public void testEnded(TestIdentifier test) {
// ignore
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
*/
public void testFailed(TestFailure status, TestIdentifier test, String trace) {
// ignore - should be impossible since this is only collecting test information
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
*/
public void testRunEnded(long elapsedTime) {
// ignore
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
*/
public void testRunFailed(String errorMessage) {
mErrorMessage = errorMessage;
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
*/
public void testRunStarted(int testCount) {
mTotalTestCount = testCount;
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
*/
public void testRunStopped(long elapsedTime) {
// ignore
}
/* (non-Javadoc)
* @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
*/
public void testStarted(TestIdentifier test) {
TestSuiteReference suiteRef = mTestTree.get(test.getClassName());
if (suiteRef == null) {
// this test suite has not been seen before, create it
suiteRef = new TestSuiteReference(test.getClassName());
mTestTree.put(test.getClassName(), suiteRef);
}
suiteRef.addTest(new TestCaseReference(test));
}
/**
* Returns the total test count in the test run.
*/
public int getTestCaseCount() {
return mTotalTestCount;
}
/**
* Sends info about the test tree to be executed (ie the suites and their enclosed tests)
*
* @param notified the {@link IVisitsTestTrees} to send test data to
*/
public void sendTrees(IVisitsTestTrees notified) {
for (ITestReference ref : mTestTree.values()) {
ref.sendTree(notified);
}
}
/**
* Returns the error message that was reported when collecting test info.
* Returns <code>null</code> if no error occurred.
*/
public String getErrorMessage() {
return mErrorMessage;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2009 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.launch.junit.runtime;
import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
import java.util.List;
import java.util.ArrayList;
/**
* Reference for an Android test suite aka class.
*/
@SuppressWarnings("restriction")
class TestSuiteReference extends AndroidTestReference {
private final String mClassName;
private List<TestCaseReference> mTests;
/**
* Creates a TestSuiteReference
*
* @param className the fully qualified name of the test class
*/
TestSuiteReference(String className) {
mClassName = className;
mTests = new ArrayList<TestCaseReference>();
}
/**
* Returns a count of the number of test cases included in this suite.
*/
public int countTestCases() {
return mTests.size();
}
/**
* Sends test identifier and test count information for this test class, and all its included
* test methods.
*
* @param notified the {@link IVisitsTestTrees} to send test info too
*/
public void sendTree(IVisitsTestTrees notified) {
notified.visitTreeEntry(getIdentifier(), true, countTestCases());
for (TestCaseReference ref : mTests) {
ref.sendTree(notified);
}
}
/**
* Return the name of this test class.
*/
public String getName() {
return mClassName;
}
/**
* Adds a test method to this suite.
*
* @param testRef the {@link TestCaseReference} to add
*/
void addTest(TestCaseReference testRef) {
mTests.add(testRef);
}
}

View File

@@ -20,8 +20,10 @@ import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
@@ -34,12 +36,14 @@ import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.JavaRuntime;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class to manipulate Project parameters/properties.
@@ -679,4 +683,71 @@ public final class ProjectHelper {
return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
}
/**
* Find the list of projects on which this JavaProject is dependent on at the compilation level.
*
* @param javaProject Java project that we are looking for the dependencies.
* @return A list of Java projects for which javaProject depend on.
* @throws JavaModelException
*/
public static List<IJavaProject> getAndroidProjectDependencies(IJavaProject javaProject)
throws JavaModelException {
String[] requiredProjectNames = javaProject.getRequiredProjectNames();
// Go from java project name to JavaProject name
IJavaModel javaModel = javaProject.getJavaModel();
// loop through all dependent projects and keep only those that are Android projects
List<IJavaProject> projectList = new ArrayList<IJavaProject>(requiredProjectNames.length);
for (String javaProjectName : requiredProjectNames) {
IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName);
//Verify that the project has also the Android Nature
try {
if (!androidJavaProject.getProject().hasNature(AndroidConstants.NATURE)) {
continue;
}
} catch (CoreException e) {
continue;
}
projectList.add(androidJavaProject);
}
return projectList;
}
/**
* Returns the android package file as an IFile object for the specified
* project.
* @param project The project
* @return The android package as an IFile object or null if not found.
*/
public static IFile getApplicationPackage(IProject project) {
// get the output folder
IFolder outputLocation = BaseProjectHelper.getOutputFolder(project);
if (outputLocation == null) {
AdtPlugin.printErrorToConsole(project,
"Failed to get the output location of the project. Check build path properties"
);
return null;
}
// get the package path
String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
IResource r = outputLocation.findMember(packageName);
// check the package is present
if (r instanceof IFile && r.exists()) {
return (IFile)r;
}
String msg = String.format("Could not find %1$s!", packageName);
AdtPlugin.printErrorToConsole(project, msg);
return null;
}
}

View File

@@ -487,6 +487,15 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit
IJavaProject javaProject = projects.get(i);
IProject iProject = javaProject.getProject();
// check if the project is opened
if (iProject.isOpen() == false) {
// remove from the list
// we do not increment i in this case.
projects.remove(i);
continue;
}
// get the target from the project and its paths
IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
if (target == null) {

View File

@@ -0,0 +1,170 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import com.android.ide.eclipse.common.AndroidConstants;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.FileEditorInput;
/*
* Quick Reference Link:
* http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html
* and
* http://www.ibm.com/developerworks/opensource/library/os-ecjdt/
*/
/**
* Action executed when the "Extract String" menu item is invoked.
* <p/>
* The intent of the action is to start a refactoring that extracts a source string and
* replaces it by an Android string resource ID.
* <p/>
* Workflow:
* <ul>
* <li> The action is currently located in the Refactoring menu in the main menu.
* <li> TODO: extend the popup refactoring menu in a Java or Android XML file.
* <li> The action is only enabled if the selection is 1 character or more. That is at least part
* of the string must be selected, it's not enough to just move the insertion point. This is
* a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when
* the insertion point is merely moved. TODO: address this limitation.
* <ul> The action gets the current {@link ISelection}. It also knows the current
* {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the
* actual resource file. By looking at the Active Window > Active Page > Active Editor we
* can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file)
* that is being edited.
* <ul> TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit}
* can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow
* us to be able to work with a selection from an Android XML file later.
* <li> The action creates a new {@link ExtractStringRefactoring} and make it run on in a new
* {@link ExtractStringWizard}.
* <ul>
*/
public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
/** Keep track of the current workbench window. */
private IWorkbenchWindow mWindow;
private ITextSelection mSelection;
private IFile mFile;
/**
* Keep track of the current workbench window.
*/
public void init(IWorkbenchWindow window) {
mWindow = window;
}
public void dispose() {
// Nothing to do
}
/**
* Examine the selection to determine if the action should be enabled or not.
* <p/>
* Keep a link to the relevant selection structure (i.e. a part of the Java AST).
*/
public void selectionChanged(IAction action, ISelection selection) {
// Note, two kinds of selections are returned here:
// ITextSelection on a Java source window
// IStructuredSelection in the outline or navigator
// This simply deals with the refactoring based on a non-empty selection.
// At that point, just enable the action and later decide if it's valid when it actually
// runs since we don't have access to the AST yet.
mSelection = null;
mFile = null;
if (selection instanceof ITextSelection) {
mSelection = (ITextSelection) selection;
if (mSelection.getLength() > 0) {
mFile = getSelectedFile();
}
}
action.setEnabled(mSelection != null && mFile != null);
}
/**
* Create a new instance of our refactoring and a wizard to configure it.
*/
public void run(IAction action) {
if (mSelection != null && mFile != null) {
ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mSelection);
RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject());
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
} catch (InterruptedException e) {
// Interrupted. Pass.
}
}
}
/**
* Returns the active {@link IFile} (hopefully matching our selection) or null.
* The file is only returned if it's a file from a project with an Android nature.
* <p/>
* At that point we do not try to analyze if the selection nor the file is suitable
* for the refactoring. This check is performed when the refactoring is invoked since
* it can then produce meaningful error messages as needed.
*/
private IFile getSelectedFile() {
IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (wwin != null) {
IWorkbenchPage page = wwin.getActivePage();
if (page != null) {
IEditorPart editor = page.getActiveEditor();
if (editor != null) {
IEditorInput input = editor.getEditorInput();
if (input instanceof FileEditorInput) {
FileEditorInput fi = (FileEditorInput) input;
IFile file = fi.getFile();
if (file.exists()) {
IProject proj = file.getProject();
try {
if (proj != null && proj.hasNature(AndroidConstants.NATURE)) {
return file;
}
} catch (CoreException e) {
// ignore
}
}
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import org.eclipse.ltk.core.refactoring.RefactoringContribution;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import java.util.Map;
/**
* @see ExtractStringDescriptor
*/
public class ExtractStringContribution extends RefactoringContribution {
/* (non-Javadoc)
* @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int)
*/
@SuppressWarnings("unchecked")
@Override
public RefactoringDescriptor createDescriptor(
String id,
String project,
String description,
String comment,
Map arguments,
int flags)
throws IllegalArgumentException {
return new ExtractStringDescriptor(project, description, comment, arguments);
}
@SuppressWarnings("unchecked")
@Override
public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
if (descriptor instanceof ExtractStringDescriptor) {
return ((ExtractStringDescriptor) descriptor).getArguments();
}
return super.retrieveArgumentMap(descriptor);
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import java.util.Map;
/**
* A descriptor that allows an {@link ExtractStringRefactoring} to be created from
* a previous instance of itself.
*/
public class ExtractStringDescriptor extends RefactoringDescriptor {
public static final String ID =
"com.android.ide.eclipse.adt.refactoring.extract.string"; //$NON-NLS-1$
private final Map<String, String> mArguments;
public ExtractStringDescriptor(String project, String description, String comment,
Map<String, String> arguments) {
super(ID, project, description, comment,
RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags
);
mArguments = arguments;
}
public Map<String, String> getArguments() {
return mArguments;
}
/**
* Creates a new refactoring instance for this refactoring descriptor based on
* an argument map. The argument map is created by the refactoring itself in
* {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)}
* <p/>
* This is apparently used to replay a refactoring.
*
* {@inheritDoc}
*
* @throws CoreException
*/
@Override
public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
try {
ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments);
return ref;
} catch (NullPointerException e) {
status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor");
return null;
}
}
}

View File

@@ -0,0 +1,477 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
import com.android.ide.eclipse.editors.wizards.ConfigurationSelector;
import com.android.sdklib.SdkConstants;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @see ExtractStringRefactoring
*/
class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
/** Last res file path used, shared across the session instances but specific to the
* current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */
private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>();
/** The project where the user selection happened. */
private final IProject mProject;
/** Field displaying the user-selected string to be replaced. */
private Label mStringLabel;
/** Test field where the user enters the new ID to be generated or replaced with. */
private Text mNewIdTextField;
/** The configuration selector, to select the resource path of the XML file. */
private ConfigurationSelector mConfigSelector;
/** The combo to display the existing XML files or enter a new one. */
private Combo mResFileCombo;
/** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
* a leaf file name ending with .xml */
private static final Pattern RES_XML_FILE_REGEX = Pattern.compile(
"/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$
/** Absolute destination folder root, e.g. "/res/" */
private static final String RES_FOLDER_ABS =
AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
/** Relative destination folder root, e.g. "res/" */
private static final String RES_FOLDER_REL =
SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml";
public ExtractStringInputPage(IProject project) {
super("ExtractStringInputPage"); //$NON-NLS-1$
mProject = project;
}
/**
* Create the UI for the refactoring wizard.
* <p/>
* Note that at that point the initial conditions have been checked in
* {@link ExtractStringRefactoring}.
*/
public void createControl(Composite parent) {
Composite content = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 1;
content.setLayout(layout);
createStringReplacementGroup(content);
createResFileGroup(content);
validatePage();
setControl(content);
}
/**
* Creates the top group with the field to replace which string and by what
* and by which options.
*
* @param content A composite with a 1-column grid layout
*/
private void createStringReplacementGroup(Composite content) {
final ExtractStringRefactoring ref = getOurRefactoring();
Group group = new Group(content, SWT.NONE);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setText("String Replacement");
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
// line: String found in selection
Label label = new Label(group, SWT.NONE);
label.setText("String:");
String selectedString = ref.getTokenString();
mStringLabel = new Label(group, SWT.NONE);
mStringLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mStringLabel.setText(selectedString != null ? selectedString : "");
// TODO provide an option to replace all occurences of this string instead of
// just the one.
// line : Textfield for new ID
label = new Label(group, SWT.NONE);
label.setText("Replace by R.string.");
mNewIdTextField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
mNewIdTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mNewIdTextField.setText(guessId(selectedString));
ref.setReplacementStringId(mNewIdTextField.getText().trim());
mNewIdTextField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
if (validatePage()) {
ref.setReplacementStringId(mNewIdTextField.getText().trim());
}
}
});
}
/**
* Creates the lower group with the fields to choose the resource confirmation and
* the target XML file.
*
* @param content A composite with a 1-column grid layout
*/
private void createResFileGroup(Composite content) {
Group group = new Group(content, SWT.NONE);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setText("XML resource to edit");
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
// line: selection of the res config
Label label;
label = new Label(group, SWT.NONE);
label.setText("Configuration:");
mConfigSelector = new ConfigurationSelector(group);
GridData gd = new GridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
gd.widthHint = ConfigurationSelector.WIDTH_HINT;
gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
mConfigSelector.setLayoutData(gd);
OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
// line: selection of the output file
label = new Label(group, SWT.NONE);
label.setText("Resource file:");
mResFileCombo = new Combo(group, SWT.DROP_DOWN);
mResFileCombo.select(0);
mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mResFileCombo.addModifyListener(onConfigSelectorUpdated);
// set output file name to the last one used
String projPath = mProject.getFullPath().toPortableString();
String filePath = sLastResFilePath.get(projPath);
mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
onConfigSelectorUpdated.run();
}
/**
* Utility method to guess a suitable new XML ID based on the selected string.
*/
private String guessId(String text) {
// make lower case
text = text.toLowerCase();
// everything not alphanumeric becomes an underscore
text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
// the id must be a proper Java identifier, so it can't start with a number
if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) {
text = "_" + text; //$NON-NLS-1$
}
return text;
}
/**
* Returns the {@link ExtractStringRefactoring} instance used by this wizard page.
*/
private ExtractStringRefactoring getOurRefactoring() {
return (ExtractStringRefactoring) getRefactoring();
}
/**
* Validates fields of the wizard input page. Displays errors as appropriate and
* enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}.
*
* @return True if the page has been positively validated. It may still have warnings.
*/
private boolean validatePage() {
boolean success = true;
// Analyze fatal errors.
String text = mNewIdTextField.getText().trim();
if (text == null || text.length() < 1) {
setErrorMessage("Please provide a resource ID to replace with.");
success = false;
} else {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
boolean ok = i == 0 ?
Character.isJavaIdentifierStart(c) :
Character.isJavaIdentifierPart(c);
if (!ok) {
setErrorMessage(String.format(
"The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.",
c, i+1));
success = false;
break;
}
}
}
String resFile = mResFileCombo.getText();
if (success) {
if (resFile == null || resFile.length() == 0) {
setErrorMessage("A resource file name is required.");
success = false;
} else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) {
setErrorMessage("The XML file name is not valid.");
success = false;
}
}
// Analyze info & warnings.
if (success) {
setErrorMessage(null);
ExtractStringRefactoring ref = getOurRefactoring();
ref.setTargetFile(resFile);
sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
if (ref.isResIdDuplicate(resFile, text)) {
setMessage(
String.format("There's already a string item called '%1$s' in %2$s.",
text, resFile),
WizardPage.WARNING);
} else if (mProject.findMember(resFile) == null) {
setMessage(
String.format("File %2$s does not exist and will be created.",
text, resFile),
WizardPage.INFORMATION);
} else {
setMessage(null);
}
}
setPageComplete(success);
return success;
}
public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
/** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
private final Pattern mPathRegex = Pattern.compile(
"(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$
/** Temporary config object used to retrieve the Config Selector value. */
private FolderConfiguration mTempConfig = new FolderConfiguration();
private HashMap<String, TreeSet<String>> mFolderCache =
new HashMap<String, TreeSet<String>>();
private String mLastFolderUsedInCombo = null;
private boolean mInternalConfigChange;
private boolean mInternalFileComboChange;
/**
* Callback invoked when the {@link ConfigurationSelector} has been changed.
* <p/>
* The callback does the following:
* <ul>
* <li> Examine the current file name to retrieve the XML filename, if any.
* <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/).
* <li> Examine the path to retrieve all the files in it. Keep those in a local cache.
* <li> If the XML filename from step 1 is not in the file list, it's a custom file name.
* Insert it and sort it.
* <li> Re-populate the file combo with all the choices.
* <li> Select the original XML file.
*/
public void run() {
if (mInternalConfigChange) {
return;
}
// get current leafname, if any
String leafName = "";
String currPath = mResFileCombo.getText();
Matcher m = mPathRegex.matcher(currPath);
if (m.matches()) {
// Note: groups 1 and 2 cannot be null.
leafName = m.group(2);
currPath = m.group(1);
} else {
// There was a path but it was invalid. Ignore it.
currPath = "";
}
// recreate the res path from the current configuration
mConfigSelector.getConfiguration(mTempConfig);
StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
sb.append('/');
String newPath = sb.toString();
if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
// Path has not changed. No need to reload.
return;
}
// Get all the files at the new path
TreeSet<String> filePaths = mFolderCache.get(newPath);
if (filePaths == null) {
filePaths = new TreeSet<String>();
IFolder folder = mProject.getFolder(newPath);
if (folder != null && folder.exists()) {
try {
for (IResource res : folder.members()) {
String name = res.getName();
if (res.getType() == IResource.FILE && name.endsWith(".xml")) {
filePaths.add(newPath + name);
}
}
} catch (CoreException e) {
// Ignore.
}
}
mFolderCache.put(newPath, filePaths);
}
currPath = newPath + leafName;
if (leafName.length() > 0 && !filePaths.contains(currPath)) {
filePaths.add(currPath);
}
// Fill the combo
try {
mInternalFileComboChange = true;
mResFileCombo.removeAll();
for (String filePath : filePaths) {
mResFileCombo.add(filePath);
}
int index = -1;
if (leafName.length() > 0) {
index = mResFileCombo.indexOf(currPath);
if (index >= 0) {
mResFileCombo.select(index);
}
}
if (index == -1) {
mResFileCombo.setText(currPath);
}
mLastFolderUsedInCombo = newPath;
} finally {
mInternalFileComboChange = false;
}
// finally validate the whole page
validatePage();
}
/**
* Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been
* modified.
*/
public void modifyText(ModifyEvent e) {
if (mInternalFileComboChange) {
return;
}
String wsFolderPath = mResFileCombo.getText();
// This is a custom path, we need to sanitize it.
// First it should start with "/res/". Then we need to make sure there are no
// relative paths, things like "../" or "./" or even "//".
wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$
wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$
wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
// We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
mInternalFileComboChange = true;
mResFileCombo.setText(wsFolderPath);
mInternalFileComboChange = false;
}
if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
if (pos >= 0) {
wsFolderPath = wsFolderPath.substring(0, pos);
}
String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
if (folderSegments.length > 0) {
String folderName = folderSegments[0];
if (folderName != null && !folderName.equals(wsFolderPath)) {
// update config selector
mInternalConfigChange = true;
mConfigSelector.setConfiguration(folderSegments);
mInternalConfigChange = false;
}
}
}
validatePage();
}
}
}

View File

@@ -0,0 +1,965 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.AndroidXPathFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
/**
* This refactoring extracts a string from a file and replaces it by an Android resource ID
* such as R.string.foo.
* <p/>
* There are a number of scenarios, which are not all supported yet. The workflow works as
* such:
* <ul>
* <li> User selects a string in a Java (TODO: or XML file) and invokes
* the {@link ExtractStringAction}.
* <li> The action finds the {@link ICompilationUnit} being edited as well as the current
* {@link ITextSelection}. The action creates a new instance of this refactoring as
* well as an {@link ExtractStringWizard} and runs the operation.
* <li> TODO: to support refactoring from an XML file, the action should give the {@link IFile}
* and then here we would have to determine whether it's a suitable Android XML file or a
* suitable Java file.
* TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout
* files or text elements (e.g. <string>foo</string>) for values, etc.
* <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
* that the java source is not read-only and is in sync. We also try to find a string under
* the selection. If this fails, the refactoring is aborted.
* <li> TODO: Find the string in an XML file based on selection.
* <li> On success, the wizard is shown, which let the user input the new ID to use.
* <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
* ID, the XML file to update, etc. The wizard does use the utility method
* {@link #isResIdDuplicate(String, String)} to check whether the new ID is already defined
* in the target XML file.
* <li> Once Preview or Finish is selected in the wizard, the
* {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
* and compute the actual changes.
* <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
* </ul>
*
* The list of changes are:
* <ul>
* <li> If the target XML does not exist, create it with the new string ID.
* <li> If the target XML exists, find the <resources> node and add the new string ID right after.
* If the node is <resources/>, it needs to be opened.
* <li> Create an AST rewriter to edit the source Java file and replace all occurences by the
* new computed R.string.foo. Also need to rewrite imports to import R as needed.
* If there's already a conflicting R included, we need to insert the FQCN instead.
* <li> TODO: If the source is an XML file, determine if we need to change an attribute or a
* a text element.
* <li> TODO: Have a pref in the wizard: [x] Change other XML Files
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
* </ul>
*/
class ExtractStringRefactoring extends Refactoring {
/** The file model being manipulated. */
private final IFile mFile;
/** The start of the selection in {@link #mFile}. */
private final int mSelectionStart;
/** The end of the selection in {@link #mFile}. */
private final int mSelectionEnd;
/** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
private ICompilationUnit mUnit;
/** The actual string selected, after UTF characters have been escaped, good for display. */
private String mTokenString;
/** The XML string ID selected by the user in the wizard. */
private String mXmlStringId;
/** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
* in the wizard. */
private String mTargetXmlFileWsPath;
/** A temporary cache of R.string IDs defined by a given xml file. The key is the
* project path of the file, the data is a set of known string Ids for that file. */
private HashMap<String,HashSet<String>> mResIdCache;
/** An instance of XPath, created lazily on demand. */
private XPath mXPath;
/** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
* used by {@link #createChange(IProgressMonitor)}. */
private ArrayList<Change> mChanges;
public ExtractStringRefactoring(Map<String, String> arguments)
throws NullPointerException {
IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
}
private Map<String, String> createArgumentMap() {
HashMap<String, String> args = new HashMap<String, String>();
args.put("file", mFile.getFullPath().toPortableString()); //$NON-NLS-1$
args.put("sel-start", Integer.toString(mSelectionStart)); //$NON-NLS-1$
args.put("sel-end", Integer.toString(mSelectionEnd)); //$NON-NLS-1$
args.put("tok-esc", mTokenString); //$NON-NLS-1$
return args;
}
public ExtractStringRefactoring(IFile file, ITextSelection selection) {
mFile = file;
mSelectionStart = selection.getOffset();
mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
}
/**
* @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
*/
@Override
public String getName() {
return "Extract Android String";
}
/**
* Gets the actual string selected, after UTF characters have been escaped,
* good for display.
*/
public String getTokenString() {
return mTokenString;
}
/**
* Step 1 of 3 of the refactoring:
* Checks that the current selection meets the initial condition before the ExtractString
* wizard is shown. The check is supposed to be lightweight and quick. Note that at that
* point the wizard has not been created yet.
* <p/>
* Here we scan the source buffer to find the token matching the selection.
* The check is successful is a Java string literal is selected, the source is in sync
* and is not read-only.
* <p/>
* This is also used to extract the string to be modified, so that we can display it in
* the refactoring wizard.
*
* @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
*
* @throws CoreException
*/
@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
mUnit = null;
mTokenString = null;
RefactoringStatus status = new RefactoringStatus();
try {
monitor.beginTask("Checking preconditions...", 5);
if (!checkSourceFile(mFile, status, monitor)) {
return status;
}
// Try to get a compilation unit from this file. If it fails, mUnit is null.
try {
mUnit = JavaCore.createCompilationUnitFrom(mFile);
// Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar
if (mUnit.isReadOnly()) {
status.addFatalError("The file is read-only, please make it writeable first.");
return status;
}
// This is a Java file. Check if it contains the selection we want.
if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
return status;
}
} catch (Exception e) {
// That was not a Java file. Ignore.
}
if (mUnit == null) {
// Check this an XML file and get the selection and its context.
// TODO
status.addFatalError("Selection must be inside a Java source file.");
}
} finally {
monitor.done();
}
return status;
}
/**
* Try to find the selected Java element in the compilation unit.
*
* If selection matches a string literal, capture it, otherwise add a fatal error
* to the status.
*
* On success, advance the monitor by 3.
*/
private boolean findSelectionInJavaUnit(ICompilationUnit unit,
RefactoringStatus status, IProgressMonitor monitor) {
try {
IBuffer buffer = unit.getBuffer();
IScanner scanner = ToolFactory.createScanner(
false, //tokenizeComments
false, //tokenizeWhiteSpace
false, //assertMode
false //recordLineSeparator
);
scanner.setSource(buffer.getCharacters());
monitor.worked(1);
for(int token = scanner.getNextToken();
token != ITerminalSymbols.TokenNameEOF;
token = scanner.getNextToken()) {
if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
// found the token, but only keep of the right type
if (token == ITerminalSymbols.TokenNameStringLiteral) {
mTokenString = new String(scanner.getCurrentTokenSource());
}
break;
} else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
// scanner is past the selection, abort.
break;
}
}
} catch (JavaModelException e1) {
// Error in unit.getBuffer. Ignore.
} catch (InvalidInputException e2) {
// Error in scanner.getNextToken. Ignore.
} finally {
monitor.worked(1);
}
if (mTokenString != null) {
// As a literal string, the token should have surrounding quotes. Remove them.
int len = mTokenString.length();
if (len > 0 &&
mTokenString.charAt(0) == '"' &&
mTokenString.charAt(len - 1) == '"') {
mTokenString = mTokenString.substring(1, len - 1);
}
// We need a non-empty string literal
if (mTokenString.length() == 0) {
mTokenString = null;
}
}
if (mTokenString == null) {
status.addFatalError("Please select a Java string literal.");
}
monitor.worked(1);
return status.isOK();
}
/**
* Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
* Might not be useful.
*
* On success, advance the monitor by 2.
*
* @return False if caller should abort, true if caller should continue.
*/
private boolean checkSourceFile(IFile file,
RefactoringStatus status,
IProgressMonitor monitor) {
// check whether the source file is in sync
if (!file.isSynchronized(IResource.DEPTH_ZERO)) {
status.addFatalError("The file is not synchronized. Please save it first.");
return false;
}
monitor.worked(1);
// make sure we can write to it.
ResourceAttributes resAttr = file.getResourceAttributes();
if (resAttr == null || resAttr.isReadOnly()) {
status.addFatalError("The file is read-only, please make it writeable first.");
return false;
}
monitor.worked(1);
return true;
}
/**
* Step 2 of 3 of the refactoring:
* Check the conditions once the user filled values in the refactoring wizard,
* then prepare the changes to be applied.
* <p/>
* In this case, most of the sanity checks are done by the wizard so essentially this
* should only be called if the wizard positively validated the user input.
*
* Here we do check that the target resource XML file either does not exists or
* is not read-only.
*
* @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
*
* @throws CoreException
*/
@Override
public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();
try {
monitor.beginTask("Checking post-conditions...", 3);
if (mXmlStringId == null || mXmlStringId.length() <= 0) {
// this is not supposed to happen
status.addFatalError("Missing replacement string ID");
} else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) {
// this is not supposed to happen
status.addFatalError("Missing target xml file path");
}
monitor.worked(1);
// Either that resource must not exist or it must be a writeable file.
IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
if (targetXml != null) {
if (targetXml.getType() != IResource.FILE) {
status.addFatalError(
String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath));
} else {
ResourceAttributes attr = targetXml.getResourceAttributes();
if (attr != null && attr.isReadOnly()) {
status.addFatalError(
String.format("XML file '%1$s' is read-only.",
mTargetXmlFileWsPath));
}
}
}
monitor.worked(1);
if (status.hasError()) {
return status;
}
mChanges = new ArrayList<Change>();
// Prepare the change for the XML file.
if (!isResIdDuplicate(mTargetXmlFileWsPath, mXmlStringId)) {
// We actually change it only if the ID doesn't exist yet
Change change = createXmlChange((IFile) targetXml, mXmlStringId, mTokenString,
status, SubMonitor.convert(monitor, 1));
if (change != null) {
mChanges.add(change);
}
}
if (status.hasError()) {
return status;
}
// Prepare the change to the Java compilation unit
List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
status, SubMonitor.convert(monitor, 1));
if (changes != null) {
mChanges.addAll(changes);
}
monitor.worked(1);
} finally {
monitor.done();
}
return status;
}
/**
* Internal helper that actually prepares the {@link Change} that adds the given
* ID to the given XML File.
* <p/>
* This does not actually modify the file.
*
* @param targetXml The file resource to modify.
* @param xmlStringId The new ID to insert.
* @param tokenString The old string, which will be the value in the XML string.
* @return A new {@link TextEdit} that describes how to change the file.
*/
private Change createXmlChange(IFile targetXml,
String xmlStringId,
String tokenString,
RefactoringStatus status,
SubMonitor subMonitor) {
TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
xmlChange.setTextType("xml"); //$NON-NLS-1$
TextEdit edit = null;
TextEditGroup editGroup = null;
if (!targetXml.exists()) {
// The XML file does not exist. Simply create it.
StringBuilder content = new StringBuilder();
content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
content.append("<resources>\n"); //$NON-NLS-1$
content.append(" <string name=\""). //$NON-NLS-1$
append(xmlStringId).
append("\">"). //$NON-NLS-1$
append(tokenString).
append("</string>\n"); //$NON-NLS-1$
content.append("<resources>\n"); //$NON-NLS-1$
edit = new InsertEdit(0, content.toString());
editGroup = new TextEditGroup("Create ID in new XML file", edit);
} else {
// The file exist. Attempt to parse it as a valid XML document.
try {
int[] indices = new int[2];
if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$
// Indices[1] indicates whether we found > or />. It can only be 1 or 2.
// Indices[0] is the position of the first character of either > or />.
//
// Note: we don't even try to adapt our formatting to the existing structure (we
// could by capturing whatever whitespace is after the closing bracket and
// applying it here before our tag, unless we were dealing with an empty
// resource tag.)
int offset = indices[0];
int len = indices[1];
StringBuilder content = new StringBuilder();
content.append(">\n"); //$NON-NLS-1$
content.append(" <string name=\""). //$NON-NLS-1$
append(xmlStringId).
append("\">"). //$NON-NLS-1$
append(tokenString).
append("</string>"); //$NON-NLS-1$
if (len == 2) {
content.append("\n</resources>"); //$NON-NLS-1$
}
edit = new ReplaceEdit(offset, len, content.toString());
editGroup = new TextEditGroup("Insert ID in XML file", edit);
}
} catch (CoreException e) {
// Failed to read file. Ignore. Will return null below.
}
}
if (edit == null) {
status.addFatalError(String.format("Failed to modify file %1$s",
mTargetXmlFileWsPath));
return null;
}
xmlChange.setEdit(edit);
// The TextEditChangeGroup let the user toggle this change on and off later.
xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
subMonitor.worked(1);
return xmlChange;
}
/**
* Parse an XML input stream, looking for an opening tag.
* <p/>
* If found, returns the character offest in the buffer of the closing bracket of that
* tag, e.g. the position of > in "<resources>". The first character is at offset 0.
* <p/>
* The implementation here relies on a simple character-based parser. No DOM nor SAX
* parsing is used, due to the simplified nature of the task: we just want the first
* opening tag, which in our case should be the document root. We deal however with
* with the tag being commented out, so comments are skipped. We assume the XML doc
* is sane, e.g. we don't expect the tag to appear in the middle of a string. But
* again since in fact we want the root element, that's unlikely to happen.
* <p/>
* We need to deal with the case where the element is written as <resources/>, in
* which case the caller will want to replace /> by ">...</...>". To do that we return
* two values: the first offset of the closing tag (e.g. / or >) and the length, which
* can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >.
*
* @param contents An existing buffer to parse.
* @param tag The tag to look for.
* @param indices The return values: [0] is the offset of the closing bracket and [1] is
* the length which can be only 1 for > and 2 for />
* @return True if we found the tag, in which case <code>indices</code> can be used.
*/
private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) {
BufferedReader br = new BufferedReader(new InputStreamReader(contents));
StringBuilder sb = new StringBuilder(); // scratch area
tag = "<" + tag;
int tagLen = tag.length();
int maxLen = tagLen < 3 ? 3 : tagLen;
try {
int offset = 0;
int i = 0;
char searching = '<'; // we want opening tags
boolean capture = false;
boolean inComment = false;
boolean inTag = false;
while ((i = br.read()) != -1) {
char c = (char) i;
if (c == searching) {
capture = true;
}
if (capture) {
sb.append(c);
int len = sb.length();
if (inComment && c == '>') {
// is the comment being closed?
if (len >= 3 && sb.substring(len-3).equals("-->")) { //$NON-NLS-1$
// yes, comment is closing, stop capturing
capture = false;
inComment = false;
sb.setLength(0);
}
} else if (inTag && c == '>') {
// we're capturing in our tag, waiting for the closing >, we just got it
// so we're totally done here. Simply detect whether it's /> or >.
indices[0] = offset;
indices[1] = 1;
if (sb.charAt(len - 2) == '/') {
indices[0]--;
indices[1]++;
}
return true;
} else if (!inComment && !inTag) {
// not a comment and not our tag yet, so we're capturing because a
// tag is being opened but we don't know which one yet.
// look for either the opening or a comment or
// the opening of our tag.
if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$
inComment = true;
} else if (len == tagLen && sb.toString().equals(tag)) {
inTag = true;
}
// if we're not interested in this tag yet, deal with when to stop
// capturing: the opening tag ends with either any kind of whitespace
// or with a > or maybe there's a PI that starts with <?
if (!inComment && !inTag) {
if (c == '>' || c == '?' || c == ' ' || c == '\n' || c == '\r') {
// stop capturing
capture = false;
sb.setLength(0);
}
}
}
if (capture && len > maxLen) {
// in any case we don't need to capture more than the size of our tag
// or the comment opening tag
sb.deleteCharAt(0);
}
}
offset++;
}
} catch (IOException e) {
// Ignore.
} finally {
try {
br.close();
} catch (IOException e) {
// oh come on...
}
}
return false;
}
/**
* Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
*/
private List<Change> computeJavaChanges(ICompilationUnit unit,
String xmlStringId,
String tokenString,
RefactoringStatus status,
SubMonitor subMonitor) {
// Get the Android package name from the Android Manifest. We need it to create
// the FQCN of the R class.
String packageName = null;
String error = null;
IProject proj = unit.getJavaProject().getProject();
IResource manifestFile = proj.findMember(AndroidConstants.FN_ANDROID_MANIFEST);
if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
error = "File not found";
} else {
try {
AndroidManifestParser manifest = AndroidManifestParser.parseForData(
(IFile) manifestFile);
if (manifest == null) {
error = "Invalid content";
} else {
packageName = manifest.getPackage();
if (packageName == null) {
error = "Missing package definition";
}
}
} catch (CoreException e) {
error = e.getLocalizedMessage();
}
}
if (error != null) {
status.addFatalError(
String.format("Failed to parse file %1$s: %2$s.",
manifestFile.getFullPath(), error));
return null;
}
// TODO in a future version we might want to collect various Java files that
// need to be updated in the same project and process them all together.
// To do that we need to use an ASTRequestor and parser.createASTs, kind of
// like this:
//
// ASTRequestor requestor = new ASTRequestor() {
// @Override
// public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) {
// super.acceptAST(sourceUnit, astNode);
// // TODO process astNode
// }
// };
// ...
// parser.createASTs(compilationUnits, bindingKeys, requestor, monitor)
//
// and then add multiple TextFileChange to the changes arraylist.
// Right now the changes array will contain one TextFileChange at most.
ArrayList<Change> changes = new ArrayList<Change>();
// This is the unit that will be modified.
TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
change.setTextType("java"); //$NON-NLS-1$
// Create an AST for this compilation unit
ASTParser parser = ASTParser.newParser(AST.JLS3);
parser.setProject(unit.getJavaProject());
parser.setSource(unit);
parser.setResolveBindings(true);
ASTNode node = parser.createAST(subMonitor.newChild(1));
// The ASTNode must be a CompilationUnit, by design
if (!(node instanceof CompilationUnit)) {
status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$
node.getClass()));
return null;
}
// ImportRewrite will allow us to add the new type to the imports and will resolve
// what the Java source must reference, e.g. the FQCN or just the simple name.
ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
String Rqualifier = packageName + ".R"; //$NON-NLS-1$
Rqualifier = importRewrite.addImport(Rqualifier);
// Rewrite the AST itself via an ASTVisitor
AST ast = node.getAST();
ASTRewrite astRewrite = ASTRewrite.create(ast);
ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
ast, astRewrite, astEditGroups,
tokenString, Rqualifier, xmlStringId);
node.accept(visitor);
// Finally prepare the change set
try {
MultiTextEdit edit = new MultiTextEdit();
// Create the edit to change the imports, only if anything changed
TextEdit subEdit = importRewrite.rewriteImports(subMonitor.newChild(1));
if (subEdit.hasChildren()) {
edit.addChild(subEdit);
}
// Create the edit to change the Java source, only if anything changed
subEdit = astRewrite.rewriteAST();
if (subEdit.hasChildren()) {
edit.addChild(subEdit);
}
// Only create a change set if any edit was collected
if (edit.hasChildren()) {
change.setEdit(edit);
// Create TextEditChangeGroups which let the user turn changes on or off
// individually. This must be done after the change.setEdit() call above.
for (TextEditGroup editGroup : astEditGroups) {
change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
}
changes.add(change);
}
// TODO to modify another Java source, loop back to the creation of the
// TextFileChange and accumulate in changes. Right now only one source is
// modified.
if (changes.size() > 0) {
return changes;
}
subMonitor.worked(1);
} catch (CoreException e) {
// ImportRewrite.rewriteImports failed.
status.addFatalError(e.getMessage());
}
return null;
}
public class ReplaceStringsVisitor extends ASTVisitor {
private final AST mAst;
private final ASTRewrite mRewriter;
private final String mOldString;
private final String mRQualifier;
private final String mXmlId;
private final ArrayList<TextEditGroup> mEditGroups;
public ReplaceStringsVisitor(AST ast,
ASTRewrite astRewrite,
ArrayList<TextEditGroup> editGroups,
String oldString,
String rQualifier,
String xmlId) {
mAst = ast;
mRewriter = astRewrite;
mEditGroups = editGroups;
mOldString = oldString;
mRQualifier = rQualifier;
mXmlId = xmlId;
}
@Override
public boolean visit(StringLiteral node) {
if (node.getLiteralValue().equals(mOldString)) {
Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$
SimpleName idName = mAst.newSimpleName(mXmlId);
QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName);
TextEditGroup editGroup = new TextEditGroup("Replace string by ID");
mEditGroups.add(editGroup);
mRewriter.replace(node, newNode, editGroup);
}
return super.visit(node);
}
}
/**
* Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
* work and creates a descriptor that can be used to replay that refactoring later.
*
* @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
*
* @throws CoreException
*/
@Override
public Change createChange(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
try {
monitor.beginTask("Applying changes...", 1);
CompositeChange change = new CompositeChange(
getName(),
mChanges.toArray(new Change[mChanges.size()])) {
@Override
public ChangeDescriptor getDescriptor() {
String comment = String.format(
"Extracts string '%1$s' into R.string.%2$s",
mTokenString,
mXmlStringId);
ExtractStringDescriptor desc = new ExtractStringDescriptor(
mUnit.getJavaProject().getElementName(), //project
comment, //description
comment, //comment
createArgumentMap());
return new RefactoringChangeDescriptor(desc);
}
};
monitor.worked(1);
return change;
} finally {
monitor.done();
}
}
/**
* Utility method used by the wizard to check whether the given string ID is already
* defined in the XML file which path is given.
*
* @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
* The given file may or may not exist.
* @param stringId The string ID to find.
* @return True if such a string ID is already defined.
*/
public boolean isResIdDuplicate(String xmlFileWsPath, String stringId) {
// This is going to be called many times on the same file.
// Build a cache of the existing IDs for a given file.
if (mResIdCache == null) {
mResIdCache = new HashMap<String, HashSet<String>>();
}
HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
if (cache == null) {
cache = getResIdsForFile(xmlFileWsPath);
mResIdCache.put(xmlFileWsPath, cache);
}
return cache.contains(stringId);
}
/**
* Extract all the defined string IDs from a given file using XPath.
*
* @param xmlFileWsPath The project path of the file to parse. It may not exist.
* @return The set of all string IDs defined in the file. The returned set is always non
* null. It is empty if the file does not exist.
*/
private HashSet<String> getResIdsForFile(String xmlFileWsPath) {
HashSet<String> ids = new HashSet<String>();
if (mXPath == null) {
mXPath = AndroidXPathFactory.newXPath();
}
// Access the project that contains the resource that contains the compilation unit
IResource resource = getTargetXmlResource(xmlFileWsPath);
if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
InputSource source;
try {
source = new InputSource(((IFile) resource).getContents());
// We want all the IDs in an XML structure like this:
// <resources>
// <string name="ID">something</string>
// </resources>
String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$
Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
if (result instanceof NodeList) {
NodeList list = (NodeList) result;
for (int n = list.getLength() - 1; n >= 0; n--) {
String id = list.item(n).getNodeValue();
ids.add(id);
}
}
} catch (CoreException e1) {
// IFile.getContents failed. Ignore.
} catch (XPathExpressionException e) {
// mXPath.evaluate failed. Ignore.
}
}
return ids;
}
/**
* Given a file project path, returns its resource in the same project than the
* compilation unit. The resource may not exist.
*/
private IResource getTargetXmlResource(String xmlFileWsPath) {
IProject proj = mFile.getProject();
IResource resource = proj.getFile(xmlFileWsPath);
return resource;
}
/**
* Sets the replacement string ID. Used by the wizard to set the user input.
*/
public void setReplacementStringId(String replacementStringId) {
mXmlStringId = replacementStringId;
}
/**
* Sets the target file. This is a project path, e.g. "/res/values/strings.xml".
* Used by the wizard to set the user input.
*/
public void setTargetFile(String targetXmlFileWsPath) {
mTargetXmlFileWsPath = targetXmlFileWsPath;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2009 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.refactorings.extractstring;
import org.eclipse.core.resources.IProject;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
/**
* A wizard for ExtractString based on a simple dialog with one page.
*
* @see ExtractStringInputPage
* @see ExtractStringRefactoring
*/
class ExtractStringWizard extends RefactoringWizard {
private final IProject mProject;
/**
* Create a wizard for ExtractString based on a simple dialog with one page.
*
* @param ref The instance of {@link ExtractStringRefactoring} to associate to the wizard.
* @param project The project where the wizard was invoked from (e.g. where the user selection
* happened, so that we can retrieve project resources.)
*/
public ExtractStringWizard(ExtractStringRefactoring ref, IProject project) {
super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
mProject = project;
setDefaultPageTitle(ref.getName());
}
@Override
protected void addUserInputPages() {
addPage(new ExtractStringInputPage(mProject));
}
}

View File

@@ -157,7 +157,7 @@ public class LayoutParamsParser {
superClasses[2] = paramsClassName;
}
HashMap<String, ArrayList<IClassDescriptor>> found =
mClassLoader.findClassesDerivingFrom("android.", superClasses);
mClassLoader.findClassesDerivingFrom("android.", superClasses); //$NON-NLS-1$
mTopViewClass = mClassLoader.getClass(rootClassName);
mTopGroupClass = mClassLoader.getClass(groupClassName);
if (paramsClassName != null) {
@@ -179,8 +179,7 @@ public class LayoutParamsParser {
addView(mTopViewClass);
// ViewGroup derives from View
mGroupMap.get(groupClassName).setSuperClass(
mViewMap.get(rootClassName));
mGroupMap.get(groupClassName).setSuperClass(mViewMap.get(rootClassName));
progress.setWorkRemaining(mGroupList.size() + mViewList.size());
@@ -346,7 +345,7 @@ public class LayoutParamsParser {
private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) {
IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses();
for (IClassDescriptor innerClass : innerClasses) {
if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_LAYOUTPARAMS)) {
if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_NAME_LAYOUTPARAMS)) {
return innerClass;
}
}

View File

@@ -22,7 +22,9 @@
package com.android.ide.eclipse.adt.wizards.newproject;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.sdklib.IAndroidTarget;
@@ -122,6 +124,7 @@ public class NewProjectCreationPage extends WizardPage {
private Button mCreateActivityCheck;
private Text mMinSdkVersionField;
private SdkTargetSelector mSdkTargetSelector;
private ITargetChangeListener mSdkTargetChangeListener;
private boolean mInternalLocationPathUpdate;
protected boolean mInternalProjectNameUpdate;
@@ -263,6 +266,17 @@ public class NewProjectCreationPage extends WizardPage {
// Validate. This will complain about the first empty field.
setPageComplete(validatePage());
}
@Override
public void dispose() {
if (mSdkTargetChangeListener != null) {
AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
mSdkTargetChangeListener = null;
}
super.dispose();
}
/**
* Creates the group for the project name:
@@ -389,18 +403,35 @@ public class NewProjectCreationPage extends WizardPage {
group.setFont(parent.getFont());
group.setText("Target");
// get the targets from the sdk
IAndroidTarget[] targets = null;
if (Sdk.getCurrent() != null) {
targets = Sdk.getCurrent().getTargets();
}
// The selector is created without targets. They are added below in the change listener.
mSdkTargetSelector = new SdkTargetSelector(group, null, false /*multi-selection*/);
mSdkTargetSelector = new SdkTargetSelector(group, targets, false /*multi-selection*/);
mSdkTargetChangeListener = new ITargetChangeListener() {
public void onProjectTargetChange(IProject changedProject) {
// Ignore
}
// If there's only one target, select it
if (targets != null && targets.length == 1) {
mSdkTargetSelector.setSelection(targets[0]);
}
public void onTargetsLoaded() {
// 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
if (targets != null && targets.length == 1) {
mSdkTargetSelector.setSelection(targets[0]);
}
}
};
AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
// Invoke it once to initialize the targets
mSdkTargetChangeListener.onTargetsLoaded();
mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
@Override
@@ -829,7 +860,7 @@ public class NewProjectCreationPage extends WizardPage {
String packageName = null;
String activityName = null;
int minSdkVersion = 0; // 0 means no minSdkVersion provided in the manifest
int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
try {
packageName = manifestData.getPackage();
minSdkVersion = manifestData.getApiLevelRequirement();
@@ -927,7 +958,7 @@ public class NewProjectCreationPage extends WizardPage {
}
}
if (!foundTarget && minSdkVersion > 0) {
if (!foundTarget && minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK) {
try {
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (target.getApiVersionNumber() == minSdkVersion) {
@@ -954,7 +985,8 @@ public class NewProjectCreationPage extends WizardPage {
if (!foundTarget) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(
minSdkVersion <= 0 ? "" : Integer.toString(minSdkVersion)); //$NON-NLS-1$
minSdkVersion == AndroidManifestParser.INVALID_MIN_SDK ? "" :
Integer.toString(minSdkVersion)); //$NON-NLS-1$
mInternalMinSdkVersionUpdate = false;
}
}
@@ -1148,7 +1180,7 @@ public class NewProjectCreationPage extends WizardPage {
return MSG_NONE;
}
int version = -1;
int version = AndroidManifestParser.INVALID_MIN_SDK;
try {
// If not empty, it must be a valid integer > 0
version = Integer.parseInt(getMinSdkVersion());

View File

@@ -180,6 +180,8 @@ public class AndroidConstants {
public final static String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$
public final static String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$
public final static String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$
public final static String CLASS_INSTRUMENTATION_RUNNER =
"android.test.InstrumentationTestRunner"; //$NON-NLS-1$
public final static String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$
public final static String CLASS_R = "android.R"; //$NON-NLS-1$
public final static String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$
@@ -187,14 +189,16 @@ public class AndroidConstants {
public final static String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$
public final static String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$
public final static String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$
public final static String CLASS_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
public final static String CLASS_NAME_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
public final static String CLASS_VIEWGROUP_LAYOUTPARAMS =
CLASS_VIEWGROUP + "$" + CLASS_LAYOUTPARAMS; //$NON-NLS-1$
public final static String CLASS_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
CLASS_VIEWGROUP + "$" + CLASS_NAME_LAYOUTPARAMS; //$NON-NLS-1$
public final static String CLASS_NAME_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
public final static String CLASS_FRAMELAYOUT =
"android.widget." + CLASS_NAME_FRAMELAYOUT; //$NON-NLS-1$
public final static String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$
public final static String CLASS_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
public final static String CLASS_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
public final static String CLASS_PREFERENCES =
"android.preference." + CLASS_PREFERENCE_SCREEN; //$NON-NLS-1$
"android.preference." + CLASS_NAME_PREFERENCE_SCREEN; //$NON-NLS-1$
public final static String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$
public final static String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$
@@ -215,4 +219,5 @@ public class AndroidConstants {
/** The base URL where to find the Android class & manifest documentation */
public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$
public static final String LIBRARY_TEST_RUNNER = "android.test.runner"; // $NON-NLS-1$
}

View File

@@ -16,6 +16,7 @@
package com.android.ide.eclipse.common.project;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener;
import com.android.sdklib.SdkConstants;
@@ -33,6 +34,7 @@ import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
@@ -72,6 +74,8 @@ public class AndroidManifestParser {
private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$
private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$
public final static int INVALID_MIN_SDK = -1;
/**
* XML error & data handler used when parsing the AndroidManifest.xml file.
* <p/>
@@ -92,8 +96,9 @@ public class AndroidManifestParser {
private Set<String> mProcesses = null;
/** debuggable attribute value. If null, the attribute is not present. */
private Boolean mDebuggable = null;
/** API level requirement. if 0 the attribute was not present. */
private int mApiLevelRequirement = 0;
/** API level requirement. if {@link AndroidManifestParser#INVALID_MIN_SDK}
* the attribute was not present. */
private int mApiLevelRequirement = INVALID_MIN_SDK;
/** List of all instrumentations declared by the manifest */
private final ArrayList<String> mInstrumentations = new ArrayList<String>();
/** List of all libraries in use declared by the manifest */
@@ -171,7 +176,8 @@ public class AndroidManifestParser {
}
/**
* Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
* Returns the <code>minSdkVersion</code> attribute, or
* {@link AndroidManifestParser#INVALID_MIN_SDK} if it's not set.
*/
int getApiLevelRequirement() {
return mApiLevelRequirement;
@@ -251,12 +257,8 @@ public class AndroidManifestParser {
} catch (NumberFormatException e) {
handleError(e, -1 /* lineNumber */);
}
} else if (NODE_INSTRUMENTATION.equals(localName)) {
value = getAttributeValue(attributes, ATTRIBUTE_NAME,
true /* hasNamespace */);
if (value != null) {
mInstrumentations.add(value);
}
} else if (NODE_INSTRUMENTATION.equals(localName)) {
processInstrumentationNode(attributes);
}
break;
case LEVEL_ACTIVITY:
@@ -445,6 +447,25 @@ public class AndroidManifestParser {
addProcessName(processName);
}
}
/**
* Processes the instrumentation nodes.
* @param attributes the attributes for the activity node.
* node is representing
*/
private void processInstrumentationNode(Attributes attributes) {
// lets get the class name, and check it if required.
String instrumentationName = getAttributeValue(attributes, ATTRIBUTE_NAME,
true /* hasNamespace */);
if (instrumentationName != null) {
String instrClassName = combinePackageAndClassName(mPackage, instrumentationName);
mInstrumentations.add(instrClassName);
if (mMarkErrors) {
checkClass(instrClassName, AndroidConstants.CLASS_INSTRUMENTATION,
true /* testVisibility */);
}
}
}
/**
* Checks that a class is valid and can be used in the Android Manifest.
@@ -480,8 +501,7 @@ public class AndroidManifestParser {
} catch (CoreException e) {
}
}
}
}
}
/**
@@ -561,25 +581,41 @@ public class AndroidManifestParser {
ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
errorListener, gatherData, javaProject, markErrors);
parser.parse(new InputSource(manifestFile.getContents()), manifestHandler);
// get the result from the handler
return new AndroidManifestParser(manifestHandler.getPackage(),
manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
manifestHandler.getActivities(),
manifestHandler.getLauncherActivity(),
manifestHandler.getProcesses(),
manifestHandler.getDebuggable(),
manifestHandler.getApiLevelRequirement(),
manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s: %s",
manifestFile.getFullPath(),
e.getMessage());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s: %s",
manifestFile.getFullPath(),
e.getMessage());
} catch (IOException e) {
} finally {
}
// Don't log a console error when failing to read a non-existing file
if (!(e instanceof FileNotFoundException)) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s: %s",
manifestFile.getFullPath(),
e.getMessage());
}
}
return null;
}
/**
* Parses the Android Manifest, and returns an object containing the result of the parsing.
* <p/>
@@ -610,16 +646,33 @@ public class AndroidManifestParser {
// get the result from the handler
return new AndroidManifestParser(manifestHandler.getPackage(),
manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
manifestHandler.getActivities(),
manifestHandler.getLauncherActivity(),
manifestHandler.getProcesses(),
manifestHandler.getDebuggable(),
manifestHandler.getApiLevelRequirement(),
manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s: %s",
manifestFile.getAbsolutePath(),
e.getMessage());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s: %s",
manifestFile.getAbsolutePath(),
e.getMessage());
} catch (IOException e) {
} finally {
// Don't log a console error when failing to read a non-existing file
if (!(e instanceof FileNotFoundException)) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s: %s",
manifestFile.getAbsolutePath(),
e.getMessage());
}
}
return null;
}
@@ -642,10 +695,12 @@ public class AndroidManifestParser {
boolean gatherData,
boolean markErrors)
throws CoreException {
IFile manifestFile = getManifest(javaProject.getProject());
try {
SAXParser parser = sParserFactory.newSAXParser();
IFile manifestFile = getManifest(javaProject.getProject());
if (manifestFile != null) {
ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
errorListener, gatherData, javaProject, markErrors);
@@ -660,10 +715,15 @@ public class AndroidManifestParser {
manifestHandler.getInstrumentations(), manifestHandler.getUsesLibraries());
}
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s", manifestFile.getFullPath());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s", manifestFile.getFullPath());
} catch (IOException e) {
} finally {
}
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s", manifestFile.getFullPath());
}
return null;
}
@@ -750,7 +810,8 @@ public class AndroidManifestParser {
}
/**
* Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
* Returns the <code>minSdkVersion</code> attribute, or {@link #INVALID_MIN_SDK}
* if it's not set.
*/
public int getApiLevelRequirement() {
return mApiLevelRequirement;

View File

@@ -16,8 +16,7 @@
package com.android.ide.eclipse.common.project;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jdt.core.IJavaModel;
@@ -82,7 +81,7 @@ public class ProjectChooserHelper {
// open the dialog and return the object selected if OK was clicked, or null otherwise
if (dialog.open() == Window.OK) {
return (IJavaProject)dialog.getFirstResult();
return (IJavaProject) dialog.getFirstResult();
}
return null;
}
@@ -107,4 +106,24 @@ public class ProjectChooserHelper {
return mAndroidProjects;
}
/**
* Helper method to get the Android project with the given name
*
* @param projectName the name of the project to find
* @return the {@link IProject} for the Android project. <code>null</code> if not found.
*/
public IProject getAndroidProject(String projectName) {
IProject iproject = null;
IJavaProject[] javaProjects = getAndroidProjects(null);
if (javaProjects != null) {
for (IJavaProject javaProject : javaProjects) {
if (javaProject.getElementName().equals(projectName)) {
iproject = javaProject.getProject();
break;
}
}
}
return iproject;
}
}

View File

@@ -70,6 +70,18 @@ public class DeclareStyleableInfo {
mFormats = formats;
}
/**
* @param name The XML Name of the attribute
* @param formats The formats of the attribute. Cannot be null.
* Should have at least one format.
* @param javadoc Short javadoc (i.e. the first sentence).
*/
public AttributeInfo(String name, Format[] formats, String javadoc) {
mName = name;
mFormats = formats;
mJavaDoc = javadoc;
}
public AttributeInfo(AttributeInfo info) {
mName = info.mName;
mFormats = info.mFormats;

View File

@@ -24,6 +24,7 @@ import com.android.sdklib.SdkConstants;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.graphics.Image;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -221,6 +222,18 @@ public class ElementDescriptor {
mChildren = newChildren;
}
/** Sets the list of allowed children.
* <p/>
* This is just a convenience method that converts a Collection into an array and
* calls {@link #setChildren(ElementDescriptor[])}.
* <p/>
* This means a <em>copy</em> of the collection is made. The collection is not
* stored by the recipient and can thus be altered by the caller.
*/
public void setChildren(Collection<ElementDescriptor> newChildren) {
setChildren(newChildren.toArray(new ElementDescriptor[newChildren.size()]));
}
/**
* Returns an optional tooltip. Will be null if not present.
* <p/>

View File

@@ -61,8 +61,10 @@ import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.LanguageReg
import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.MobileCodeVerifier;
import com.android.layoutlib.api.ILayoutLog;
import com.android.layoutlib.api.ILayoutResult;
import com.android.layoutlib.api.IProjectCallback;
import com.android.layoutlib.api.IResourceValue;
import com.android.layoutlib.api.IStyleResourceValue;
import com.android.layoutlib.api.IXmlPullParser;
import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
import com.android.sdklib.IAndroidTarget;
@@ -222,7 +224,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// updateUiFromFramework will reset language/region combo, so we must call
// setConfiguration after, or the settext on language/region will be lost.
if (mEditedConfig != null) {
setConfiguration(mEditedConfig);
setConfiguration(mEditedConfig, false /*force*/);
}
// make sure we remove the custom view loader, since its parent class loader is the
@@ -867,7 +869,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
@Override
void editNewFile(FolderConfiguration configuration) {
// update the configuration UI
setConfiguration(configuration);
setConfiguration(configuration, true /*force*/);
// enable the create button if the current and edited config are not equals
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
@@ -975,18 +977,14 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
int themeIndex = mThemeCombo.getSelectionIndex();
if (themeIndex != -1) {
String theme = mThemeCombo.getItem(themeIndex);
// change the string if it's a custom theme to make sure we can
// differentiate them
if (themeIndex >= mPlatformThemeCount) {
theme = "*" + theme; //$NON-NLS-1$
}
// Render a single object as described by the ViewElementDescriptor.
WidgetPullParser parser = new WidgetPullParser(descriptor);
ILayoutResult result = bridge.bridge.computeLayout(parser,
ILayoutResult result = computeLayout(bridge, parser,
null /* projectKey */,
300 /* width */, 300 /* height */, theme,
300 /* width */, 300 /* height */, 160 /*density*/,
160.f /*xdpi*/, 160.f /*ydpi*/, theme,
themeIndex >= mPlatformThemeCount /*isProjectTheme*/,
configuredProjectResources, frameworkResources, projectCallback,
null /* logger */);
@@ -1073,11 +1071,14 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
/**
* Update the UI controls state with a given {@link FolderConfiguration}.
* <p/>If a qualifier is not present in the {@link FolderConfiguration} object, the UI control
* is not modified. However if the value in the control is not the default value, a warning
* icon is showed.
* <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect
* <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>,
* the UI control is not modified. However if the value in the control is not the default value,
* a warning icon is shown.
* @param config The {@link FolderConfiguration} to set.
* @param force Whether the UI should be changed to exactly match the received configuration.
*/
void setConfiguration(FolderConfiguration config) {
void setConfiguration(FolderConfiguration config, boolean force) {
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting new values in the widgets.
mEditedConfig = config;
@@ -1088,6 +1089,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (countryQualifier != null) {
mCountry.setText(String.format("%1$d", countryQualifier.getCode()));
mCurrentConfig.setCountryCodeQualifier(countryQualifier);
} else if (force) {
mCountry.setText(""); //$NON-NLS-1$
mCurrentConfig.setCountryCodeQualifier(null);
} else if (mCountry.getText().length() > 0) {
mCountryIcon.setImage(mWarningImage);
}
@@ -1097,6 +1101,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (networkQualifier != null) {
mNetwork.setText(String.format("%1$d", networkQualifier.getCode()));
mCurrentConfig.setNetworkCodeQualifier(networkQualifier);
} else if (force) {
mNetwork.setText(""); //$NON-NLS-1$
mCurrentConfig.setNetworkCodeQualifier(null);
} else if (mNetwork.getText().length() > 0) {
mNetworkIcon.setImage(mWarningImage);
}
@@ -1106,6 +1113,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (languageQualifier != null) {
mLanguage.setText(languageQualifier.getValue());
mCurrentConfig.setLanguageQualifier(languageQualifier);
} else if (force) {
mLanguage.setText(""); //$NON-NLS-1$
mCurrentConfig.setLanguageQualifier(null);
} else if (mLanguage.getText().length() > 0) {
mLanguageIcon.setImage(mWarningImage);
}
@@ -1115,6 +1125,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (regionQualifier != null) {
mRegion.setText(regionQualifier.getValue());
mCurrentConfig.setRegionQualifier(regionQualifier);
} else if (force) {
mRegion.setText(""); //$NON-NLS-1$
mCurrentConfig.setRegionQualifier(null);
} else if (mRegion.getText().length() > 0) {
mRegionIcon.setImage(mWarningImage);
}
@@ -1125,6 +1138,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mOrientation.select(
ScreenOrientation.getIndex(orientationQualifier.getValue()) + 1);
mCurrentConfig.setScreenOrientationQualifier(orientationQualifier);
} else if (force) {
mOrientation.select(0);
mCurrentConfig.setScreenOrientationQualifier(null);
} else if (mOrientation.getSelectionIndex() != 0) {
mOrientationIcon.setImage(mWarningImage);
}
@@ -1134,6 +1150,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (densityQualifier != null) {
mDensity.setText(String.format("%1$d", densityQualifier.getValue()));
mCurrentConfig.setPixelDensityQualifier(densityQualifier);
} else if (force) {
mDensity.setText(""); //$NON-NLS-1$
mCurrentConfig.setPixelDensityQualifier(null);
} else if (mDensity.getText().length() > 0) {
mDensityIcon.setImage(mWarningImage);
}
@@ -1143,6 +1162,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (touchQualifier != null) {
mTouch.select(TouchScreenType.getIndex(touchQualifier.getValue()) + 1);
mCurrentConfig.setTouchTypeQualifier(touchQualifier);
} else if (force) {
mTouch.select(0);
mCurrentConfig.setTouchTypeQualifier(null);
} else if (mTouch.getSelectionIndex() != 0) {
mTouchIcon.setImage(mWarningImage);
}
@@ -1152,6 +1174,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (keyboardQualifier != null) {
mKeyboard.select(KeyboardState.getIndex(keyboardQualifier.getValue()) + 1);
mCurrentConfig.setKeyboardStateQualifier(keyboardQualifier);
} else if (force) {
mKeyboard.select(0);
mCurrentConfig.setKeyboardStateQualifier(null);
} else if (mKeyboard.getSelectionIndex() != 0) {
mKeyboardIcon.setImage(mWarningImage);
}
@@ -1161,6 +1186,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (inputQualifier != null) {
mTextInput.select(TextInputMethod.getIndex(inputQualifier.getValue()) + 1);
mCurrentConfig.setTextInputMethodQualifier(inputQualifier);
} else if (force) {
mTextInput.select(0);
mCurrentConfig.setTextInputMethodQualifier(null);
} else if (mTextInput.getSelectionIndex() != 0) {
mTextInputIcon.setImage(mWarningImage);
}
@@ -1171,6 +1199,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mNavigation.select(
NavigationMethod.getIndex(navigationQualifiter.getValue()) + 1);
mCurrentConfig.setNavigationMethodQualifier(navigationQualifiter);
} else if (force) {
mNavigation.select(0);
mCurrentConfig.setNavigationMethodQualifier(null);
} else if (mNavigation.getSelectionIndex() != 0) {
mNavigationIcon.setImage(mWarningImage);
}
@@ -1181,6 +1212,10 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mSize1.setText(String.format("%1$d", sizeQualifier.getValue1()));
mSize2.setText(String.format("%1$d", sizeQualifier.getValue2()));
mCurrentConfig.setScreenDimensionQualifier(sizeQualifier);
} else if (force) {
mSize1.setText(""); //$NON-NLS-1$
mSize2.setText(""); //$NON-NLS-1$
mCurrentConfig.setScreenDimensionQualifier(null);
} else if (mSize1.getText().length() > 0 && mSize2.getText().length() > 0) {
mSizeIcon.setImage(mWarningImage);
}
@@ -1607,7 +1642,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// at this point, we have not opened a new file.
// update the configuration icons with the new edited config.
setConfiguration(mEditedConfig);
setConfiguration(mEditedConfig, false /*force*/);
// enable the create button if the current and edited config are not equals
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
@@ -1794,45 +1829,16 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// Compute the layout
UiElementPullParser parser = new UiElementPullParser(getModel());
Rectangle rect = getBounds();
ILayoutResult result = null;
if (bridge.apiLevel >= 3) {
// call the new api with proper theme differentiator and
// density/dpi support.
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
// FIXME pass the density/dpi from somewhere (resource config or skin).
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, 160, 160.f, 160.f,
theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else if (bridge.apiLevel == 2) {
// api with boolean for separation of project/framework theme
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
// FIXME pass the density/dpi from somewhere (resource config or skin).
ILayoutResult result = computeLayout(bridge, parser,
iProject /* projectKey */,
rect.width, rect.height, 160, 160.f, 160.f,
theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else {
// oldest api with no density/dpi, and project theme boolean mixed
// into the theme name.
// change the string if it's a custom theme to make sure we can
// differentiate them
if (themeIndex >= mPlatformThemeCount) {
theme = "*" + theme; //$NON-NLS-1$
}
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, theme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
}
// update the UiElementNode with the layout info.
if (result.getSuccess() == ILayoutResult.SUCCESS) {
model.setEditData(result.getImage());
@@ -2383,4 +2389,48 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
return null;
}
/**
* Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
* the implementation API level.
*/
@SuppressWarnings("deprecation")
private ILayoutResult computeLayout(LayoutBridge bridge,
IXmlPullParser layoutDescription, Object projectKey,
int screenWidth, int screenHeight, int density, float xdpi, float ydpi,
String themeName, boolean isProjectTheme,
Map<String, Map<String, IResourceValue>> projectResources,
Map<String, Map<String, IResourceValue>> frameworkResources,
IProjectCallback projectCallback, ILayoutLog logger) {
if (bridge.apiLevel >= 3) {
// newer api with boolean for separation of project/framework theme,
// and density support.
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
themeName, isProjectTheme,
projectResources, frameworkResources, projectCallback,
logger);
} else if (bridge.apiLevel == 2) {
// api with boolean for separation of project/framework theme
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else {
// oldest api with no density/dpi, and project theme boolean mixed
// into the theme name.
// change the string if it's a custom theme to make sure we can
// differentiate them
if (isProjectTheme) {
themeName = "*" + themeName; //$NON-NLS-1$
}
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, themeName,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
}
}
}

View File

@@ -43,7 +43,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
public static final String ID_ATTR = "id"; //$NON-NLS-1$
/** The document descriptor. Contains all layouts and views linked together. */
private DocumentDescriptor mDescriptor =
private DocumentDescriptor mRootDescriptor =
new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$
/** The list of all known ViewLayout descriptors. */
@@ -60,7 +60,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
/** @return the document descriptor. Contains all layouts and views linked together. */
public DocumentDescriptor getDescriptor() {
return mDescriptor;
return mRootDescriptor;
}
/** @return The read-only list of all known ViewLayout descriptors. */
@@ -74,7 +74,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
}
public ElementDescriptor[] getRootElementDescriptors() {
return mDescriptor.getChildren();
return mRootDescriptor.getChildren();
}
/**
@@ -98,6 +98,10 @@ public final class LayoutDescriptors implements IDescriptorProvider {
}
}
// Create <include> as a synthetic regular view.
// Note: ViewStub is already described by attrs.xml
insertInclude(newViews);
ArrayList<ElementDescriptor> newLayouts = new ArrayList<ElementDescriptor>();
if (layouts != null) {
for (ViewClassInfo info : layouts) {
@@ -109,17 +113,22 @@ public final class LayoutDescriptors implements IDescriptorProvider {
ArrayList<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>();
newDescriptors.addAll(newLayouts);
newDescriptors.addAll(newViews);
ElementDescriptor[] newArray = newDescriptors.toArray(
new ElementDescriptor[newDescriptors.size()]);
// Link all layouts to everything else here.. recursively
for (ElementDescriptor layoutDesc : newLayouts) {
layoutDesc.setChildren(newArray);
layoutDesc.setChildren(newDescriptors);
}
// The <merge> tag can only be a root tag, so it is added at the end.
// It gets everything else as children but it is not made a child itself.
ElementDescriptor mergeTag = createMerge(newLayouts);
mergeTag.setChildren(newDescriptors); // mergeTag makes a copy of the list
newDescriptors.add(mergeTag);
newLayouts.add(mergeTag);
mViewDescriptors = newViews;
mLayoutDescriptors = newLayouts;
mDescriptor.setChildren(newArray);
mRootDescriptor.setChildren(newDescriptors);
mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors);
mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors);
@@ -186,7 +195,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
if (need_separator) {
String title;
if (layoutParams.getShortClassName().equals(
AndroidConstants.CLASS_LAYOUTPARAMS)) {
AndroidConstants.CLASS_NAME_LAYOUTPARAMS)) {
title = String.format("Layout Attributes from %1$s",
layoutParams.getViewLayoutClass().getShortClassName());
} else {
@@ -217,4 +226,99 @@ public final class LayoutDescriptors implements IDescriptorProvider {
false /* mandatory */);
}
/**
* Creates a new <include> descriptor and adds it to the list of view descriptors.
*
* @param knownViews A list of view descriptors being populated. Also used to find the
* View descriptor and extract its layout attributes.
*/
private void insertInclude(ArrayList<ElementDescriptor> knownViews) {
String xml_name = "include"; //$NON-NLS-1$
// Create the include custom attributes
ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
// Note that the "layout" attribute does NOT have the Android namespace
DescriptorsUtils.appendAttribute(attributes,
null, //elementXmlName
null, //nsUri
new AttributeInfo(
"layout", //$NON-NLS-1$
new AttributeInfo.Format[] { AttributeInfo.Format.REFERENCE }
),
true, //required
null); //overrides
DescriptorsUtils.appendAttribute(attributes,
null, //elementXmlName
SdkConstants.NS_RESOURCES, //nsUri
new AttributeInfo(
"id", //$NON-NLS-1$
new AttributeInfo.Format[] { AttributeInfo.Format.REFERENCE }
),
true, //required
null); //overrides
// Find View and inherit all its layout attributes
AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
AndroidConstants.CLASS_VIEW, knownViews);
// Create the include descriptor
ViewElementDescriptor desc = new ViewElementDescriptor(xml_name, // xml_name
xml_name, // ui_name
null, // canonical class name, we don't have one
"Lets you statically include XML layouts inside other XML layouts.", // tooltip
null, // sdk_url
attributes.toArray(new AttributeDescriptor[attributes.size()]),
viewLayoutAttribs, // layout attributes
null, // children
false /* mandatory */);
knownViews.add(desc);
}
/**
* Creates and return a new <merge> descriptor.
* @param knownLayouts A list of all known layout view descriptors, used to find the
* FrameLayout descriptor and extract its layout attributes.
*/
private ElementDescriptor createMerge(ArrayList<ElementDescriptor> knownLayouts) {
String xml_name = "merge"; //$NON-NLS-1$
// Find View and inherit all its layout attributes
AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
AndroidConstants.CLASS_FRAMELAYOUT, knownLayouts);
// Create the include descriptor
ViewElementDescriptor desc = new ViewElementDescriptor(xml_name, // xml_name
xml_name, // ui_name
null, // canonical class name, we don't have one
"A root tag useful for XML layouts inflated using a ViewStub.", // tooltip
null, // sdk_url
null, // attributes
viewLayoutAttribs, // layout attributes
null, // children
false /* mandatory */);
return desc;
}
/**
* Finds the descriptor and retrieves all its layout attributes.
*/
private AttributeDescriptor[] findViewLayoutAttributes(
String viewFqcn,
ArrayList<ElementDescriptor> knownViews) {
for (ElementDescriptor desc : knownViews) {
if (desc instanceof ViewElementDescriptor) {
ViewElementDescriptor viewDesc = (ViewElementDescriptor) desc;
if (viewFqcn.equals(viewDesc.getCanonicalClassName())) {
return viewDesc.getLayoutAttributes();
}
}
}
return null;
}
}

View File

@@ -96,6 +96,8 @@ class DropFeedback {
RelativeInfo info = null;
UiElementEditPart sibling = null;
// TODO consider merge like a vertical layout
// TODO consider TableLayout like a linear
if (LayoutConstants.LINEAR_LAYOUT.equals(layoutXmlName)) {
sibling = findLinearTarget(parentPart, where)[1];

View File

@@ -81,7 +81,7 @@ public class UiViewElementNode extends UiElementNode {
if (layoutDescriptors != null) {
for (ElementDescriptor desc : layoutDescriptors) {
if (desc instanceof ViewElementDescriptor &&
desc.getXmlName().equals(AndroidConstants.CLASS_FRAMELAYOUT)) {
desc.getXmlName().equals(AndroidConstants.CLASS_NAME_FRAMELAYOUT)) {
layout_attrs = ((ViewElementDescriptor) desc).getLayoutAttributes();
need_xmlns = true;
break;

View File

@@ -1233,8 +1233,9 @@ public class UiElementNode implements IPropertySource {
Node attr = attrs.item(n);
if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$
String uri = attr.getNodeValue();
String nsPrefix = attr.getLocalName();
if (SdkConstants.NS_RESOURCES.equals(uri)) {
String nsPrefix = attr.getLocalName();
// Is this the URI we are looking for? If yes, we found its prefix.
if (nsUri.equals(uri)) {
return nsPrefix;
}
visited.add(nsPrefix);
@@ -1244,7 +1245,8 @@ public class UiElementNode implements IPropertySource {
// Use a sensible default prefix if we can't find one.
// We need to make sure the prefix is not one that was declared in the scope
// visited above.
// visited above. Use a default namespace prefix "android" for the Android resource
// NS and use "ns" for all other custom namespaces.
String prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$
String base = prefix;
for (int i = 1; visited.contains(prefix); i++) {
@@ -1475,18 +1477,18 @@ public class UiElementNode implements IPropertySource {
}
}
}
if (attribute != null) {
final UiAttributeNode fAttribute = attribute;
// get the current value and compare it to the new value
String oldValue = fAttribute.getCurrentValue();
String oldValue = attribute.getCurrentValue();
final String newValue = (String)value;
if (oldValue.equals(newValue)) {
return;
}
final UiAttributeNode fAttribute = attribute;
AndroidEditor editor = getEditor();
editor.editXmlModel(new Runnable() {
public void run() {
@@ -1495,6 +1497,4 @@ public class UiElementNode implements IPropertySource {
});
}
}
}

View File

@@ -17,8 +17,10 @@
package com.android.ide.eclipse.editors.wizards;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.ProjectChooserHelper;
import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
@@ -39,6 +41,7 @@ import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.viewers.IStructuredSelection;
@@ -235,7 +238,7 @@ class NewXmlFileCreationPage extends WizardPage {
"An XML file that describes preferences.", // tooltip
ResourceFolderType.XML, // folder type
AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed
AndroidConstants.CLASS_PREFERENCE_SCREEN, // default root
AndroidConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root
SdkConstants.NS_RESOURCES, // xmlns
null, // default attributes
1 // target API level
@@ -271,9 +274,9 @@ class NewXmlFileCreationPage extends WizardPage {
final static int NUM_COL = 4;
/** Absolute destination folder root, e.g. "/res/" */
private static String sResFolderAbs = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
private static final String RES_FOLDER_ABS = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
/** Relative destination folder root, e.g. "res/" */
private static String sResFolderRel = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
private static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
private IProject mProject;
private Text mProjectTextField;
@@ -288,7 +291,9 @@ class NewXmlFileCreationPage extends WizardPage {
private boolean mInternalTypeUpdate;
private boolean mInternalConfigSelectorUpdate;
private ProjectChooserHelper mProjectChooserHelper;
private ITargetChangeListener mSdkTargetChangeListener;
private TypeInfo mCurrentTypeInfo;
// --- UI creation ---
@@ -335,8 +340,43 @@ class NewXmlFileCreationPage extends WizardPage {
initializeFromSelection(mInitialSelection);
initializeRootValues();
enableTypesBasedOnApi();
if (mCurrentTypeInfo != null) {
updateRootCombo(mCurrentTypeInfo);
}
installTargetChangeListener();
validatePage();
}
private void installTargetChangeListener() {
mSdkTargetChangeListener = new ITargetChangeListener() {
public void onProjectTargetChange(IProject changedProject) {
// If this is the current project, force it to reload its data
if (changedProject != null && changedProject == mProject) {
changeProject(mProject);
}
}
public void onTargetsLoaded() {
// Reload the current project, if any, in case its target has changed.
if (mProject != null) {
changeProject(mProject);
}
}
};
AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
}
@Override
public void dispose() {
if (mSdkTargetChangeListener != null) {
AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
mSdkTargetChangeListener = null;
}
super.dispose();
}
/**
* Returns the target project or null.
@@ -650,7 +690,6 @@ class NewXmlFileCreationPage extends WizardPage {
return;
}
// Find the best match in the element list. In case there are multiple selected elements
// select the one that provides the most information and assign them a score,
// e.g. project=1 + folder=2 + file=4.
@@ -745,8 +784,35 @@ class NewXmlFileCreationPage extends WizardPage {
// cleared above.
// get the AndroidTargetData from the project
IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
IAndroidTarget target = null;
AndroidTargetData data = null;
target = Sdk.getCurrent().getTarget(mProject);
if (target == null) {
// A project should have a target. The target can be missing if the project
// is an old project for which a target hasn't been affected or if the
// target no longer exists in this SDK. Simply log the error and dismiss.
AdtPlugin.log(IStatus.INFO,
"NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$
mProject.getName());
continue;
} else {
data = Sdk.getCurrent().getTargetData(target);
if (data == null) {
// We should have both a target and its data.
// However if the wizard is invoked whilst the platform is still being
// loaded we can end up in a weird case where we have a target but it
// doesn't have any data yet.
// Lets log a warning and silently ignore this root.
AdtPlugin.log(IStatus.INFO,
"NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$
target.getName(), mProject.getName());
continue;
}
}
IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
ElementDescriptor descriptor = provider.getDescriptor();
@@ -819,6 +885,10 @@ class NewXmlFileCreationPage extends WizardPage {
/**
* Changes mProject to the given new project and update the UI accordingly.
* <p/>
* Note that this does not check if the new project is the same as the current one
* on purpose, which allows a project to be updated when its target has changed or
* when targets are loaded in the background.
*/
private void changeProject(IProject newProject) {
mProject = newProject;
@@ -856,16 +926,16 @@ class NewXmlFileCreationPage extends WizardPage {
ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>();
// We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
if (wsFolderPath.startsWith(sResFolderRel)) {
wsFolderPath = sResFolderAbs + wsFolderPath.substring(sResFolderRel.length());
if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
mInternalWsFolderPathUpdate = true;
mWsFolderPathTextField.setText(wsFolderPath);
mInternalWsFolderPathUpdate = false;
}
if (wsFolderPath.startsWith(sResFolderAbs)) {
wsFolderPath = wsFolderPath.substring(sResFolderAbs.length());
if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
if (pos >= 0) {
@@ -952,16 +1022,16 @@ class NewXmlFileCreationPage extends WizardPage {
// The configuration is valid. Reformat the folder path using the canonical
// value from the configuration.
newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
} else {
// The configuration is invalid. We still update the path but this time
// do it manually on the string.
if (wsFolderPath.startsWith(sResFolderAbs)) {
if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
wsFolderPath.replaceFirst(
"^(" + sResFolderAbs +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
"^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
"\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
}
}
@@ -1018,7 +1088,7 @@ class NewXmlFileCreationPage extends WizardPage {
if (type != null) {
mConfigSelector.getConfiguration(mTempConfig);
StringBuffer sb = new StringBuffer(sResFolderAbs);
StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
sb.append(mTempConfig.getFolderName(type.getResFolderType()));
mInternalWsFolderPathUpdate = true;
@@ -1038,6 +1108,7 @@ class NewXmlFileCreationPage extends WizardPage {
private void selectType(TypeInfo type) {
if (type == null || !type.getWidget().getSelection()) {
mInternalTypeUpdate = true;
mCurrentTypeInfo = type;
for (TypeInfo type2 : sTypes) {
type2.getWidget().setSelection(type2 == type);
}
@@ -1131,8 +1202,8 @@ class NewXmlFileCreationPage extends WizardPage {
// -- validate generated path
if (error == null) {
String wsFolderPath = getWsFolderPath();
if (!wsFolderPath.startsWith(sResFolderAbs)) {
error = String.format("Target folder must start with %1$s.", sResFolderAbs);
if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) {
error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS);
}
}

View File

@@ -15,7 +15,13 @@
*/
package com.android.ide.eclipse.tests;
import com.android.ide.eclipse.common.AndroidConstants;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Logger;
@@ -45,12 +51,29 @@ public class AdtTestData {
// accessed normally
mOsRootDataPath = System.getProperty("test_data");
if (mOsRootDataPath == null) {
sLogger.info("Cannot find test_data directory, init to class loader");
sLogger.info("Cannot find test_data environment variable, init to class loader");
URL url = this.getClass().getClassLoader().getResource("data"); //$NON-NLS-1$
mOsRootDataPath = url.getFile();
if (Platform.isRunning()) {
sLogger.info("Running as an Eclipse Plug-in JUnit test, using FileLocator");
try {
mOsRootDataPath = FileLocator.resolve(url).getFile();
} catch (IOException e) {
sLogger.warning("IOException while using FileLocator, reverting to url");
mOsRootDataPath = url.getFile();
}
} else {
sLogger.info("Running as an plain JUnit test, using url as-is");
mOsRootDataPath = url.getFile();
}
}
if (mOsRootDataPath.equals(AndroidConstants.WS_SEP + "data")) {
sLogger.warning("Resource data not found using class loader!, Defaulting to no path");
}
if (!mOsRootDataPath.endsWith(File.separator)) {
sLogger.info("Fixing test_data env variable does not end with path separator");
sLogger.info("Fixing test_data env variable (does not end with path separator)");
mOsRootDataPath = mOsRootDataPath.concat(File.separator);
}
}

View File

@@ -16,17 +16,20 @@
package com.android.ide.eclipse.common.project;
import junit.framework.TestCase;
import com.android.ide.eclipse.tests.AdtTestData;
import com.android.ide.eclipse.mock.FileMock;
import junit.framework.TestCase;
/**
* Tests for {@link AndroidManifestParser}
*/
public class AndroidManifestParserTest extends TestCase {
private AndroidManifestParser mManifest;
private AndroidManifestParser mManifestTestApp;
private AndroidManifestParser mManifestInstrumentation;
private static final String PACKAGE_NAME = "com.android.testapp"; //$NON-NLS-1$
private static final String INSTRUMENTATION_XML = "AndroidManifest-instrumentation.xml"; //$NON-NLS-1$
private static final String TESTAPP_XML = "AndroidManifest-testapp.xml"; //$NON-NLS-1$
private static final String PACKAGE_NAME = "com.android.testapp"; //$NON-NLS-1$
private static final String ACTIVITY_NAME = "com.android.testapp.MainActivity"; //$NON-NLS-1$
private static final String LIBRARY_NAME = "android.test.runner"; //$NON-NLS-1$
private static final String INSTRUMENTATION_NAME = "android.test.InstrumentationTestRunner"; //$NON-NLS-1$
@@ -35,60 +38,46 @@ public class AndroidManifestParserTest extends TestCase {
protected void setUp() throws Exception {
super.setUp();
// create the test data
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
sb.append("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"); //$NON-NLS-1$
sb.append(" package=\""); //$NON-NLS-1$
sb.append(PACKAGE_NAME);
sb.append("\">\n"); //$NON-NLS-1$
sb.append(" <application android:icon=\"@drawable/icon\">\n"); //$NON-NLS-1$
sb.append(" <activity android:name=\""); //$NON-NLS-1$
sb.append(ACTIVITY_NAME);
sb.append("\" android:label=\"@string/app_name\">\n"); //$NON-NLS-1$
sb.append(" <intent-filter>\n"); //$NON-NLS-1$
sb.append(" <action android:name=\"android.intent.action.MAIN\" />\n"); //$NON-NLS-1$
sb.append(" <category android:name=\"android.intent.category.LAUNCHER\" />\"\n"); //$NON-NLS-1$
sb.append(" <category android:name=\"android.intent.category.DEFAULT\" />\n"); //$NON-NLS-1$
sb.append(" </intent-filter>\n"); //$NON-NLS-1$
sb.append(" </activity>\n"); //$NON-NLS-1$
sb.append(" <uses-library android:name=\""); //$NON-NLS-1$
sb.append(LIBRARY_NAME);
sb.append("\" />\n"); //$NON-NLS-1$
sb.append(" </application>"); //$NON-NLS-1$
sb.append(" <instrumentation android:name=\""); //$NON-NLS-1$
sb.append(INSTRUMENTATION_NAME);
sb.append("\"\n");
sb.append(" android:targetPackage=\"com.example.android.apis\"\n");
sb.append(" android:label=\"Tests for Api Demos.\"/>\n");
sb.append("</manifest>\n"); //$NON-NLS-1$
FileMock mockFile = new FileMock("AndroidManifest.xml", sb.toString().getBytes());
String testFilePath = AdtTestData.getInstance().getTestFilePath(
TESTAPP_XML);
mManifestTestApp = AndroidManifestParser.parseForData(testFilePath);
assertNotNull(mManifestTestApp);
mManifest = AndroidManifestParser.parseForData(mockFile);
assertNotNull(mManifest);
testFilePath = AdtTestData.getInstance().getTestFilePath(
INSTRUMENTATION_XML);
mManifestInstrumentation = AndroidManifestParser.parseForData(testFilePath);
assertNotNull(mManifestInstrumentation);
}
public void testGetInstrumentationInformation() {
assertEquals(1, mManifestInstrumentation.getInstrumentations().length);
assertEquals(INSTRUMENTATION_NAME, mManifestTestApp.getInstrumentations()[0]);
}
public void testGetPackage() {
assertEquals("com.android.testapp", mManifest.getPackage());
assertEquals(PACKAGE_NAME, mManifestTestApp.getPackage());
}
public void testGetActivities() {
assertEquals(1, mManifest.getActivities().length);
assertEquals(ACTIVITY_NAME, mManifest.getActivities()[0]);
assertEquals(1, mManifestTestApp.getActivities().length);
assertEquals(ACTIVITY_NAME, mManifestTestApp.getActivities()[0]);
}
public void testGetLauncherActivity() {
assertEquals(ACTIVITY_NAME, mManifest.getLauncherActivity());
assertEquals(ACTIVITY_NAME, mManifestTestApp.getLauncherActivity());
}
public void testGetUsesLibraries() {
assertEquals(1, mManifest.getUsesLibraries().length);
assertEquals(LIBRARY_NAME, mManifest.getUsesLibraries()[0]);
assertEquals(1, mManifestTestApp.getUsesLibraries().length);
assertEquals(LIBRARY_NAME, mManifestTestApp.getUsesLibraries()[0]);
}
public void testGetInstrumentations() {
assertEquals(1, mManifest.getInstrumentations().length);
assertEquals(INSTRUMENTATION_NAME, mManifest.getInstrumentations()[0]);
assertEquals(1, mManifestTestApp.getInstrumentations().length);
assertEquals(INSTRUMENTATION_NAME, mManifestTestApp.getInstrumentations()[0]);
}
public void testGetPackageName() {
assertEquals(PACKAGE_NAME, mManifestTestApp.getPackage());
}
}

View File

@@ -461,5 +461,13 @@ public class FileMock implements IFile {
public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
}
public boolean isHidden(int options) {
throw new NotImplementedException();
}
public boolean isTeamPrivateMember(int options) {
throw new NotImplementedException();
}
}

View File

@@ -74,7 +74,8 @@ public final class FolderMock implements IFolder {
// -------- UNIMPLEMENTED METHODS ----------------
public void create(boolean force, boolean local, IProgressMonitor monitor) throws CoreException {
public void create(boolean force, boolean local, IProgressMonitor monitor)
throws CoreException {
throw new NotImplementedException();
}
@@ -106,8 +107,8 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor)
throws CoreException {
public void move(IPath destination, boolean force, boolean keepHistory,
IProgressMonitor monitor) throws CoreException {
throw new NotImplementedException();
}
@@ -225,7 +226,8 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
public void deleteMarkers(String type, boolean includeSubtypes, int depth)
throws CoreException {
throw new NotImplementedException();
}
@@ -428,24 +430,31 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
public Map<?,?> getPersistentProperties() throws CoreException {
public Map<?,?> getPersistentProperties() throws CoreException {
throw new NotImplementedException();
}
}
public Map<?,?> getSessionProperties() throws CoreException {
public Map<?,?> getSessionProperties() throws CoreException {
throw new NotImplementedException();
}
}
public boolean isDerived(int options) {
public boolean isDerived(int options) {
throw new NotImplementedException();
}
}
public boolean isHidden() {
public boolean isHidden() {
throw new NotImplementedException();
}
}
public void setHidden(boolean isHidden) throws CoreException {
public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
}
}
public boolean isHidden(int options) {
throw new NotImplementedException();
}
public boolean isTeamPrivateMember(int options) {
throw new NotImplementedException();
}
}

View File

@@ -42,6 +42,14 @@ import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import java.net.URI;
import java.util.Map;
/**
* Mock implementation of {@link IProject}.
* <p/>Supported methods:
* <ul>
* <li>{@link #build(int kind, IProgressMonitor monitor)}</li>
* <li>{@link #members(int kind, String builderName, Map args, IProgressMonitor monitor)}</li>
* </ul>
*/
@SuppressWarnings("deprecation")
public class ProjectMock implements IProject {
@@ -265,7 +273,8 @@ public class ProjectMock implements IProject {
throw new NotImplementedException();
}
public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
public void deleteMarkers(String type, boolean includeSubtypes, int depth)
throws CoreException {
throw new NotImplementedException();
}
@@ -473,29 +482,36 @@ public class ProjectMock implements IProject {
throw new NotImplementedException();
}
public void create(IProjectDescription description, int updateFlags,
IProgressMonitor monitor) throws CoreException {
public void create(IProjectDescription description, int updateFlags,
IProgressMonitor monitor) throws CoreException {
throw new NotImplementedException();
}
}
public Map<?,?> getPersistentProperties() throws CoreException {
public Map<?,?> getPersistentProperties() throws CoreException {
throw new NotImplementedException();
}
}
public Map<?,?> getSessionProperties() throws CoreException {
public Map<?,?> getSessionProperties() throws CoreException {
throw new NotImplementedException();
}
}
public boolean isDerived(int options) {
public boolean isDerived(int options) {
throw new NotImplementedException();
}
}
public boolean isHidden() {
public boolean isHidden() {
throw new NotImplementedException();
}
}
public void setHidden(boolean isHidden) throws CoreException {
public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
}
}
public boolean isHidden(int options) {
throw new NotImplementedException();
}
public boolean isTeamPrivateMember(int options) {
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.AndroidProject.tests">
<!--
This declares that this app uses the instrumentation test runner targeting
the package of com.android.samples. To run the tests use the command:
"adb shell am instrument -w com.android.samples.tests/android.test.InstrumentationTestRunner"
-->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.AndroidProject"
android:label="Sample test for deployment."/>
<application>
<uses-library android:name="android.test.runner" />
</application>
</manifest>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.testapp">
<application android:icon="@drawable/icon">
<activity android:name="com.android.testapp.MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />"
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<uses-library android:name="android.test.runner"/>
</application>"
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.example.android.apis"
android:label="Tests for Api Demos."/>
</manifest>