ADT XML String Refactoring: fix refusing to edit @+id/blah.

It correctly only refuses to edit @string/blah now.

This CL also does a bit of refactoring; I extracted some methods
and a class to make it a bit easier to read.

BUG 2066460

Change-Id: I95a34d28d6390ab0cc075f05ea83ceec04993ae9
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
index c37fb6b..20ef355 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
@@ -50,26 +50,7 @@
 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.ClassInstanceCreation;
 import org.eclipse.jdt.core.dom.CompilationUnit;
-import org.eclipse.jdt.core.dom.Expression;
-import org.eclipse.jdt.core.dom.IMethodBinding;
-import org.eclipse.jdt.core.dom.ITypeBinding;
-import org.eclipse.jdt.core.dom.IVariableBinding;
-import org.eclipse.jdt.core.dom.MethodDeclaration;
-import org.eclipse.jdt.core.dom.MethodInvocation;
-import org.eclipse.jdt.core.dom.Modifier;
-import org.eclipse.jdt.core.dom.Name;
-import org.eclipse.jdt.core.dom.SimpleName;
-import org.eclipse.jdt.core.dom.SimpleType;
-import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
-import org.eclipse.jdt.core.dom.StringLiteral;
-import org.eclipse.jdt.core.dom.Type;
-import org.eclipse.jdt.core.dom.TypeDeclaration;
-import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
-import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
-import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
 import org.eclipse.jface.text.ITextSelection;
@@ -106,7 +87,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 
 /**
  * This refactoring extracts a string from a file and replaces it by an Android resource ID
@@ -402,7 +382,8 @@
             }
 
             if (!status.isOK()) {
-                status.addFatalError("Selection must be inside a Java source or an Android Layout XML file.");
+                status.addFatalError(
+                        "Selection must be inside a Java source or an Android Layout XML file.");
             }
 
         } finally {
@@ -521,7 +502,8 @@
                     }
 
                     if (node == null) {
-                        status.addFatalError("The selection does not match any element in the XML document.");
+                        status.addFatalError(
+                                "The selection does not match any element in the XML document.");
                         return status.isOK();
                     }
 
@@ -542,79 +524,10 @@
                             sdoc.getRegionAtCharacterOffset(selStart);
                         if (region != null &&
                                 DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
-                            // The region gives us the textual representation of the XML element
-                            // where the selection starts, split using sub-regions. We now just
-                            // need to iterate through the sub-regions to find which one
-                            // contains the actual selection. We're interested in an attribute
-                            // value however when we find one we want to memorize the attribute
-                            // name that was defined just before.
-
-                            int startInRegion = selStart - region.getStartOffset();
-
-                            int nb = region.getNumberOfRegions();
-                            ITextRegionList list = region.getRegions();
-                            String currAttrValue = null;
-
-                            for (int i = 0; i < nb; i++) {
-                                ITextRegion subRegion = list.get(i);
-                                String type = subRegion.getType();
-
-                                if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
-                                    currAttrName = region.getText(subRegion);
-
-                                    // I like to select the attribute definition and invoke
-                                    // the extract string wizard. So if the selection is on
-                                    // the attribute name part, find the value that is just
-                                    // after and use it as if it were the selection.
-
-                                    if (subRegion.getStart() <= startInRegion &&
-                                            startInRegion < subRegion.getTextEnd()) {
-                                        // A well-formed attribute is composed of a name,
-                                        // an equal sign and the value. There can't be any space
-                                        // in between, which makes the parsing a lot easier.
-                                        if (i <= nb - 3 &&
-                                                DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(
-                                                                       list.get(i + 1).getType())) {
-                                            subRegion = list.get(i + 2);
-                                            type = subRegion.getType();
-                                            if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(
-                                                    type)) {
-                                                currAttrValue = region.getText(subRegion);
-                                            }
-                                        }
-                                    }
-
-                                } else if (subRegion.getStart() <= startInRegion &&
-                                        startInRegion < subRegion.getTextEnd() &&
-                                        DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
-                                    currAttrValue = region.getText(subRegion);
-                                }
-
-                                if (currAttrValue != null) {
-                                    // We found the value. Only accept it if not empty
-                                    // and if we found an attribute name before.
-                                    String text = currAttrValue;
-
-                                    // The attribute value will contain the XML quotes. Remove them.
-                                    int len = text.length();
-                                    if (len >= 2 &&
-                                            text.charAt(0) == '"' &&
-                                            text.charAt(len - 1) == '"') {
-                                        text = text.substring(1, len - 1);
-                                    } else if (len >= 2 &&
-                                            text.charAt(0) == '\'' &&
-                                            text.charAt(len - 1) == '\'') {
-                                        text = text.substring(1, len - 1);
-                                    }
-                                    if (text.length() > 0 && currAttrName != null) {
-                                        // Setting mTokenString to non-null marks the fact we
-                                        // accept this attribute.
-                                        mTokenString = text;
-                                    }
-
-                                    break;
-                                }
-                            }
+                            // Find if any sub-region representing an attribute contains the
+                            // selection. If it does, returns the name of the attribute in
+                            // currAttrName and returns the value in the field mTokenString.
+                            currAttrName = findSelectionInRegion(region, selStart);
 
                             if (mTokenString == null) {
                                 status.addFatalError(
@@ -625,67 +538,10 @@
 
                     if (mTokenString != null && node != null && currAttrName != null) {
 
-                        UiElementNode rootUiNode = editor.getUiRootNode();
-                        UiElementNode currentUiNode =
-                            rootUiNode == null ? null : rootUiNode.findXmlNode(node);
-                        ReferenceAttributeDescriptor attrDesc = null;
-
-                        if (currentUiNode != null) {
-                            // remove any namespace prefix from the attribute name
-                            String name = currAttrName;
-                            int pos = name.indexOf(':');
-                            if (pos > 0 && pos < name.length() - 1) {
-                                name = name.substring(pos + 1);
-                            }
-
-                            for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) {
-                                if (attrNode.getDescriptor().getXmlLocalName().equals(name)) {
-                                    AttributeDescriptor desc = attrNode.getDescriptor();
-                                    if (desc instanceof ReferenceAttributeDescriptor) {
-                                        attrDesc = (ReferenceAttributeDescriptor) desc;
-                                    }
-                                    break;
-                                }
-                            }
-                        }
-
-                        // The attribute descriptor is a resource reference. It must either accept
-                        // of any resource type or specifically accept string types.
-                        if (attrDesc != null &&
-                                (attrDesc.getResourceType() == null ||
-                                 attrDesc.getResourceType() == ResourceType.STRING)) {
-                            // We have one more check to do: is the current string value already
-                            // an Android XML string reference? If so, we can't edit it.
-                            if (mTokenString.startsWith("@")) { //$NON-NLS-1$
-                                int pos1 = 0;
-                                if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') {
-                                    pos1++;
-                                }
-                                int pos2 = mTokenString.indexOf('/');
-                                if (pos2 > pos1) {
-                                    String kind = mTokenString.substring(pos1 + 1, pos2);
-                                    mTokenString = null;
-                                    status.addFatalError(String.format(
-                                            "The attribute %1$s already contains a %2$s reference.",
-                                            currAttrName,
-                                            kind));
-                                }
-                            }
-
-                            if (mTokenString != null) {
-                                // We're done with all our checks. mTokenString contains the
-                                // current attribute value. We don't memorize the region nor the
-                                // attribute, however we memorize the textual attribute name so
-                                // that we can offer replacement for all its occurrences.
-                                mXmlAttributeName = currAttrName;
-                            }
-
-                        } else {
-                            mTokenString = null;
-                            status.addFatalError(String.format(
-                                    "The attribute %1$s does not accept a string reference.",
-                                    currAttrName));
-                        }
+                        // Validate that the attribute accepts a string reference.
+                        // This sets mTokenString to null by side-effect when it fails and
+                        // adds a fatal error to the status as needed.
+                        validateSelectedAttribute(editor, node, currAttrName, status);
 
                     } else {
                         // We shouldn't get here: we're missing one of the token string, the node
@@ -708,6 +564,163 @@
     }
 
     /**
+     * The region gives us the textual representation of the XML element
+     * where the selection starts, split using sub-regions. We now just
+     * need to iterate through the sub-regions to find which one
+     * contains the actual selection. We're interested in an attribute
+     * value however when we find one we want to memorize the attribute
+     * name that was defined just before.
+     *
+     * @return When the cursor is on a valid attribute name or value, returns the string of
+     * attribute name. As a side-effect, returns the value of the attribute in {@link #mTokenString}
+     */
+    private String findSelectionInRegion(IStructuredDocumentRegion region, int selStart) {
+
+        String currAttrName = null;
+
+        int startInRegion = selStart - region.getStartOffset();
+
+        int nb = region.getNumberOfRegions();
+        ITextRegionList list = region.getRegions();
+        String currAttrValue = null;
+
+        for (int i = 0; i < nb; i++) {
+            ITextRegion subRegion = list.get(i);
+            String type = subRegion.getType();
+
+            if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+                currAttrName = region.getText(subRegion);
+
+                // I like to select the attribute definition and invoke
+                // the extract string wizard. So if the selection is on
+                // the attribute name part, find the value that is just
+                // after and use it as if it were the selection.
+
+                if (subRegion.getStart() <= startInRegion &&
+                        startInRegion < subRegion.getTextEnd()) {
+                    // A well-formed attribute is composed of a name,
+                    // an equal sign and the value. There can't be any space
+                    // in between, which makes the parsing a lot easier.
+                    if (i <= nb - 3 &&
+                            DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(
+                                                   list.get(i + 1).getType())) {
+                        subRegion = list.get(i + 2);
+                        type = subRegion.getType();
+                        if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(
+                                type)) {
+                            currAttrValue = region.getText(subRegion);
+                        }
+                    }
+                }
+
+            } else if (subRegion.getStart() <= startInRegion &&
+                    startInRegion < subRegion.getTextEnd() &&
+                    DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+                currAttrValue = region.getText(subRegion);
+            }
+
+            if (currAttrValue != null) {
+                // We found the value. Only accept it if not empty
+                // and if we found an attribute name before.
+                String text = currAttrValue;
+
+                // The attribute value will contain the XML quotes. Remove them.
+                int len = text.length();
+                if (len >= 2 &&
+                        text.charAt(0) == '"' &&
+                        text.charAt(len - 1) == '"') {
+                    text = text.substring(1, len - 1);
+                } else if (len >= 2 &&
+                        text.charAt(0) == '\'' &&
+                        text.charAt(len - 1) == '\'') {
+                    text = text.substring(1, len - 1);
+                }
+                if (text.length() > 0 && currAttrName != null) {
+                    // Setting mTokenString to non-null marks the fact we
+                    // accept this attribute.
+                    mTokenString = text;
+                }
+
+                break;
+            }
+        }
+
+        return currAttrName;
+    }
+
+    /**
+     * Validates that the attribute accepts a string reference.
+     * This sets mTokenString to null by side-effect when it fails and
+     * adds a fatal error to the status as needed.
+     */
+    private void validateSelectedAttribute(AndroidEditor editor, Node node,
+            String attrName, RefactoringStatus status) {
+        UiElementNode rootUiNode = editor.getUiRootNode();
+        UiElementNode currentUiNode =
+            rootUiNode == null ? null : rootUiNode.findXmlNode(node);
+        ReferenceAttributeDescriptor attrDesc = null;
+
+        if (currentUiNode != null) {
+            // remove any namespace prefix from the attribute name
+            String name = attrName;
+            int pos = name.indexOf(':');
+            if (pos > 0 && pos < name.length() - 1) {
+                name = name.substring(pos + 1);
+            }
+
+            for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) {
+                if (attrNode.getDescriptor().getXmlLocalName().equals(name)) {
+                    AttributeDescriptor desc = attrNode.getDescriptor();
+                    if (desc instanceof ReferenceAttributeDescriptor) {
+                        attrDesc = (ReferenceAttributeDescriptor) desc;
+                    }
+                    break;
+                }
+            }
+        }
+
+        // The attribute descriptor is a resource reference. It must either accept
+        // of any resource type or specifically accept string types.
+        if (attrDesc != null &&
+                (attrDesc.getResourceType() == null ||
+                 attrDesc.getResourceType() == ResourceType.STRING)) {
+            // We have one more check to do: is the current string value already
+            // an Android XML string reference? If so, we can't edit it.
+            if (mTokenString.startsWith("@")) {                             //$NON-NLS-1$
+                int pos1 = 0;
+                if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') {
+                    pos1++;
+                }
+                int pos2 = mTokenString.indexOf('/');
+                if (pos2 > pos1) {
+                    String kind = mTokenString.substring(pos1 + 1, pos2);
+                    if (ResourceType.STRING.getName().equals(kind)) {                            //$NON-NLS-1$
+                        mTokenString = null;
+                        status.addFatalError(String.format(
+                                "The attribute %1$s already contains a %2$s reference.",
+                                attrName,
+                                kind));
+                    }
+                }
+            }
+
+            if (mTokenString != null) {
+                // We're done with all our checks. mTokenString contains the
+                // current attribute value. We don't memorize the region nor the
+                // attribute, however we memorize the textual attribute name so
+                // that we can offer replacement for all its occurrences.
+                mXmlAttributeName = attrName;
+            }
+
+        } else {
+            mTokenString = null;
+            status.addFatalError(String.format(
+                    "The attribute %1$s does not accept a string reference.",
+                    attrName));
+        }
+    }
+
+    /**
      * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
      * Might not be useful.
      *
@@ -1373,412 +1386,6 @@
         return null;
     }
 
-    public class ReplaceStringsVisitor extends ASTVisitor {
-
-        private static final String CLASS_ANDROID_CONTEXT    = "android.content.Context"; //$NON-NLS-1$
-        private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence";  //$NON-NLS-1$
-        private static final String CLASS_JAVA_STRING        = "java.lang.String";        //$NON-NLS-1$
-
-
-        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;
-        }
-
-        @SuppressWarnings("unchecked")    //$NON-NLS-1$
-        @Override
-        public boolean visit(StringLiteral node) {
-            if (node.getLiteralValue().equals(mOldString)) {
-
-                // We want to analyze the calling context to understand whether we can
-                // just replace the string literal by the named int constant (R.id.foo)
-                // or if we should generate a Context.getString() call.
-                boolean useGetResource = false;
-                useGetResource = examineVariableDeclaration(node) ||
-                                    examineMethodInvocation(node);
-
-                Name qualifierName = mAst.newName(mRQualifier + ".string");     //$NON-NLS-1$
-                SimpleName idName = mAst.newSimpleName(mXmlId);
-                ASTNode newNode = mAst.newQualifiedName(qualifierName, idName);
-                String title = "Replace string by ID";
-
-                if (useGetResource) {
-
-                    Expression context = methodHasContextArgument(node);
-                    if (context == null && !isClassDerivedFromContext(node)) {
-                        // if we don't have a class that derives from Context and
-                        // we don't have a Context method argument, then try a bit harder:
-                        // can we find a method or a field that will give us a context?
-                        context = findContextFieldOrMethod(node);
-
-                        if (context == null) {
-                            // If not, let's  write Context.getString(), which is technically
-                            // invalid but makes it a good clue on how to fix it.
-                            context = mAst.newSimpleName("Context");            //$NON-NLS-1$
-                        }
-                    }
-
-                    MethodInvocation mi2 = mAst.newMethodInvocation();
-                    mi2.setName(mAst.newSimpleName("getString"));               //$NON-NLS-1$
-                    mi2.setExpression(context);
-                    mi2.arguments().add(newNode);
-
-                    newNode = mi2;
-                    title = "Replace string by Context.getString(R.string...)";
-                }
-
-                TextEditGroup editGroup = new TextEditGroup(title);
-                mEditGroups.add(editGroup);
-                mRewriter.replace(node, newNode, editGroup);
-            }
-            return super.visit(node);
-        }
-
-        /**
-         * Examines if the StringLiteral is part of of an assignment to a string,
-         * e.g. String foo = id.
-         *
-         * The parent fragment is of syntax "var = expr" or "var[] = expr".
-         * We want the type of the variable, which is either held by a
-         * VariableDeclarationStatement ("type [fragment]") or by a
-         * VariableDeclarationExpression. In either case, the type can be an array
-         * but for us all that matters is to know whether the type is an int or
-         * a string.
-         */
-        private boolean examineVariableDeclaration(StringLiteral node) {
-            VariableDeclarationFragment fragment = findParentClass(node,
-                    VariableDeclarationFragment.class);
-
-            if (fragment != null) {
-                ASTNode parent = fragment.getParent();
-
-                Type type = null;
-                if (parent instanceof VariableDeclarationStatement) {
-                    type = ((VariableDeclarationStatement) parent).getType();
-                } else if (parent instanceof VariableDeclarationExpression) {
-                    type = ((VariableDeclarationExpression) parent).getType();
-                }
-
-                if (type instanceof SimpleType) {
-                    return isJavaString(type.resolveBinding());
-                }
-            }
-
-            return false;
-        }
-
-        /**
-         * If the expression is part of a method invocation (aka a function call) or a
-         * class instance creation (aka a "new SomeClass" constructor call), we try to
-         * find the type of the argument being used. If it is a String (most likely), we
-         * want to return true (to generate a getString() call). However if there might
-         * be a similar method that takes an int, in which case we don't want to do that.
-         *
-         * This covers the case of Activity.setTitle(int resId) vs setTitle(String str).
-         */
-        @SuppressWarnings("unchecked")  //$NON-NLS-1$
-        private boolean examineMethodInvocation(StringLiteral node) {
-
-            ASTNode parent = null;
-            List arguments = null;
-            IMethodBinding methodBinding = null;
-
-            MethodInvocation invoke = findParentClass(node, MethodInvocation.class);
-            if (invoke != null) {
-                parent = invoke;
-                arguments = invoke.arguments();
-                methodBinding = invoke.resolveMethodBinding();
-            } else {
-                ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class);
-                if (newclass != null) {
-                    parent = newclass;
-                    arguments = newclass.arguments();
-                    methodBinding = newclass.resolveConstructorBinding();
-                }
-            }
-
-            if (parent != null && arguments != null && methodBinding != null) {
-                // We want to know which argument this is.
-                // Walk up the hierarchy again to find the immediate child of the parent,
-                // which should turn out to be one of the invocation arguments.
-                ASTNode child = null;
-                for (ASTNode n = node; n != parent; ) {
-                    ASTNode p = n.getParent();
-                    if (p == parent) {
-                        child = n;
-                        break;
-                    }
-                    n = p;
-                }
-                if (child == null) {
-                    // This can't happen: a parent of 'node' must be the child of 'parent'.
-                    return false;
-                }
-
-                // Find the index
-                int index = 0;
-                for (Object arg : arguments) {
-                    if (arg == child) {
-                        break;
-                    }
-                    index++;
-                }
-
-                if (index == arguments.size()) {
-                    // This can't happen: one of the arguments of 'invoke' must be 'child'.
-                    return false;
-                }
-
-                // Eventually we want to determine if the parameter is a string type,
-                // in which case a Context.getString() call must be generated.
-                boolean useStringType = false;
-
-                // Find the type of that argument
-                ITypeBinding[] types = methodBinding.getParameterTypes();
-                if (index < types.length) {
-                    ITypeBinding type = types[index];
-                    useStringType = isJavaString(type);
-                }
-
-                // Now that we know that this method takes a String parameter, can we find
-                // a variant that would accept an int for the same parameter position?
-                if (useStringType) {
-                    String name = methodBinding.getName();
-                    ITypeBinding clazz = methodBinding.getDeclaringClass();
-                    nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) {
-                        if (methodBinding == mb2 || !mb2.getName().equals(name)) {
-                            continue;
-                        }
-                        // We found a method with the same name. We want the same parameters
-                        // except that the one at 'index' must be an int type.
-                        ITypeBinding[] types2 = mb2.getParameterTypes();
-                        int len2 = types2.length;
-                        if (types.length == len2) {
-                            for (int i = 0; i < len2; i++) {
-                                if (i == index) {
-                                    ITypeBinding type2 = types2[i];
-                                    if (!("int".equals(type2.getQualifiedName()))) {   //$NON-NLS-1$
-                                        // The argument at 'index' is not an int.
-                                        continue nextMethod;
-                                    }
-                                } else if (!types[i].equals(types2[i])) {
-                                    // One of the other arguments do not match our original method
-                                    continue nextMethod;
-                                }
-                            }
-                            // If we got here, we found a perfect match: a method with the same
-                            // arguments except the one at 'index' is an int. In this case we
-                            // don't need to convert our R.id into a string.
-                            useStringType = false;
-                            break;
-                        }
-                    }
-                }
-
-                return useStringType;
-            }
-            return false;
-        }
-
-        /**
-         * Examines if the StringLiteral is part of a method declaration (a.k.a. a function
-         * definition) which takes a Context argument.
-         * If such, it returns the name of the variable as a {@link SimpleName}.
-         * Otherwise it returns null.
-         */
-        private SimpleName methodHasContextArgument(StringLiteral node) {
-            MethodDeclaration decl = findParentClass(node, MethodDeclaration.class);
-            if (decl != null) {
-                for (Object obj : decl.parameters()) {
-                    if (obj instanceof SingleVariableDeclaration) {
-                        SingleVariableDeclaration var = (SingleVariableDeclaration) obj;
-                        if (isAndroidContext(var.getType())) {
-                            return mAst.newSimpleName(var.getName().getIdentifier());
-                        }
-                    }
-                }
-            }
-            return null;
-        }
-
-        /**
-         * Walks up the node hierarchy to find the class (aka type) where this statement
-         * is used and returns true if this class derives from android.content.Context.
-         */
-        private boolean isClassDerivedFromContext(StringLiteral node) {
-            TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
-            if (clazz != null) {
-                // This is the class that the user is currently writing, so it can't be
-                // a Context by itself, it has to be derived from it.
-                return isAndroidContext(clazz.getSuperclassType());
-            }
-            return false;
-        }
-
-        private Expression findContextFieldOrMethod(StringLiteral node) {
-            TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
-            ITypeBinding clazzType = clazz == null ? null : clazz.resolveBinding();
-            return findContextFieldOrMethod(clazzType);
-        }
-
-        private Expression findContextFieldOrMethod(ITypeBinding clazzType) {
-            TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>();
-            findContextCandidates(results, clazzType, 0 /*superType*/);
-            if (results.size() > 0) {
-                Integer bestRating = results.keySet().iterator().next();
-                return results.get(bestRating);
-            }
-            return null;
-        }
-
-        /**
-         * Find all method or fields that are candidates for providing a Context.
-         * There can be various choices amongst this class or its super classes.
-         * Sort them by rating in the results map.
-         *
-         * The best ever choice is to find a method with no argument that returns a Context.
-         * The second suitable choice is to find a Context field.
-         * The least desirable choice is to find a method with arguments. It's not really
-         * desirable since we can't generate these arguments automatically.
-         *
-         * Methods and fields from supertypes are ignored if they are private.
-         *
-         * The rating is reversed: the lowest rating integer is used for the best candidate.
-         * Because the superType argument is actually a recursion index, this makes the most
-         * immediate classes more desirable.
-         *
-         * @param results The map that accumulates the rating=>expression results. The lower
-         *                rating number is the best candidate.
-         * @param clazzType The class examined.
-         * @param superType The recursion index.
-         *                  0 for the immediate class, 1 for its super class, etc.
-         */
-        private void findContextCandidates(TreeMap<Integer, Expression> results,
-                ITypeBinding clazzType,
-                int superType) {
-            for (IMethodBinding mb : clazzType.getDeclaredMethods()) {
-                // If we're looking at supertypes, we can't use private methods.
-                if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) {
-                    continue;
-                }
-
-                if (isAndroidContext(mb.getReturnType())) {
-                    // We found a method that returns something derived from Context.
-
-                    int argsLen = mb.getParameterTypes().length;
-                    if (argsLen == 0) {
-                        // We'll favor any method that takes no argument,
-                        // That would be the best candidate ever, so we can stop here.
-                        MethodInvocation mi = mAst.newMethodInvocation();
-                        mi.setName(mAst.newSimpleName(mb.getName()));
-                        results.put(Integer.MIN_VALUE, mi);
-                        return;
-                    } else {
-                        // A method with arguments isn't as interesting since we wouldn't
-                        // know how to populate such arguments. We'll use it if there are
-                        // no other alternatives. We'll favor the one with the less arguments.
-                        Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen);
-                        if (!results.containsKey(rating)) {
-                            MethodInvocation mi = mAst.newMethodInvocation();
-                            mi.setName(mAst.newSimpleName(mb.getName()));
-                            results.put(rating, mi);
-                        }
-                    }
-                }
-            }
-
-            // A direct Context field would be more interesting than a method with
-            // arguments. Try to find one.
-            for (IVariableBinding var : clazzType.getDeclaredFields()) {
-                // If we're looking at supertypes, we can't use private field.
-                if (superType != 0 && Modifier.isPrivate(var.getModifiers())) {
-                    continue;
-                }
-
-                if (isAndroidContext(var.getType())) {
-                    // We found such a field. Let's use it.
-                    Integer rating = Integer.valueOf(superType);
-                    results.put(rating, mAst.newSimpleName(var.getName()));
-                    break;
-                }
-            }
-
-            // Examine the super class to see if we can locate a better match
-            clazzType = clazzType.getSuperclass();
-            if (clazzType != null) {
-                findContextCandidates(results, clazzType, superType + 1);
-            }
-        }
-
-        /**
-         * Walks up the node hierarchy and returns the first ASTNode of the requested class.
-         * Only look at parents.
-         *
-         * Implementation note: this is a generic method so that it returns the node already
-         * casted to the requested type.
-         */
-        @SuppressWarnings("unchecked")
-        private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) {
-            for (node = node.getParent(); node != null; node = node.getParent()) {
-                if (node.getClass().equals(clazz)) {
-                    return (T) node;
-                }
-            }
-            return null;
-        }
-
-        /**
-         * Returns true if the given type is or derives from android.content.Context.
-         */
-        private boolean isAndroidContext(Type type) {
-            if (type != null) {
-                return isAndroidContext(type.resolveBinding());
-            }
-            return false;
-        }
-
-        /**
-         * Returns true if the given type is or derives from android.content.Context.
-         */
-        private boolean isAndroidContext(ITypeBinding type) {
-            for (; type != null; type = type.getSuperclass()) {
-                if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        /**
-         * Returns true if this type binding represents a String or CharSequence type.
-         */
-        private boolean isJavaString(ITypeBinding type) {
-            for (; type != null; type = type.getSuperclass()) {
-                if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) ||
-                    CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) {
-                    return true;
-                }
-            }
-            return false;
-        }
-    }
-
     /**
      * 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.
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
new file mode 100755
index 0000000..dd0f9f4
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
@@ -0,0 +1,457 @@
+/*

+ * 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.internal.refactorings.extractstring;

+

+import org.eclipse.jdt.core.dom.AST;

+import org.eclipse.jdt.core.dom.ASTNode;

+import org.eclipse.jdt.core.dom.ASTVisitor;

+import org.eclipse.jdt.core.dom.ClassInstanceCreation;

+import org.eclipse.jdt.core.dom.Expression;

+import org.eclipse.jdt.core.dom.IMethodBinding;

+import org.eclipse.jdt.core.dom.ITypeBinding;

+import org.eclipse.jdt.core.dom.IVariableBinding;

+import org.eclipse.jdt.core.dom.MethodDeclaration;

+import org.eclipse.jdt.core.dom.MethodInvocation;

+import org.eclipse.jdt.core.dom.Modifier;

+import org.eclipse.jdt.core.dom.Name;

+import org.eclipse.jdt.core.dom.SimpleName;

+import org.eclipse.jdt.core.dom.SimpleType;

+import org.eclipse.jdt.core.dom.SingleVariableDeclaration;

+import org.eclipse.jdt.core.dom.StringLiteral;

+import org.eclipse.jdt.core.dom.Type;

+import org.eclipse.jdt.core.dom.TypeDeclaration;

+import org.eclipse.jdt.core.dom.VariableDeclarationExpression;

+import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

+import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;

+import org.eclipse.text.edits.TextEditGroup;

+

+import java.util.ArrayList;

+import java.util.List;

+import java.util.TreeMap;

+

+/**

+ * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing

+ * Java source and replace it by an Android XML string reference.

+ *

+ * @see ExtractStringRefactoring#computeJavaChanges

+ */

+class ReplaceStringsVisitor extends ASTVisitor {

+

+    private static final String CLASS_ANDROID_CONTEXT    = "android.content.Context"; //$NON-NLS-1$

+    private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence";  //$NON-NLS-1$

+    private static final String CLASS_JAVA_STRING        = "java.lang.String";        //$NON-NLS-1$

+

+

+    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;

+    }

+

+    @SuppressWarnings("unchecked")    //$NON-NLS-1$

+    @Override

+    public boolean visit(StringLiteral node) {

+        if (node.getLiteralValue().equals(mOldString)) {

+

+            // We want to analyze the calling context to understand whether we can

+            // just replace the string literal by the named int constant (R.id.foo)

+            // or if we should generate a Context.getString() call.

+            boolean useGetResource = false;

+            useGetResource = examineVariableDeclaration(node) ||

+                                examineMethodInvocation(node);

+

+            Name qualifierName = mAst.newName(mRQualifier + ".string");     //$NON-NLS-1$

+            SimpleName idName = mAst.newSimpleName(mXmlId);

+            ASTNode newNode = mAst.newQualifiedName(qualifierName, idName);

+            String title = "Replace string by ID";

+

+            if (useGetResource) {

+

+                Expression context = methodHasContextArgument(node);

+                if (context == null && !isClassDerivedFromContext(node)) {

+                    // if we don't have a class that derives from Context and

+                    // we don't have a Context method argument, then try a bit harder:

+                    // can we find a method or a field that will give us a context?

+                    context = findContextFieldOrMethod(node);

+

+                    if (context == null) {

+                        // If not, let's  write Context.getString(), which is technically

+                        // invalid but makes it a good clue on how to fix it.

+                        context = mAst.newSimpleName("Context");            //$NON-NLS-1$

+                    }

+                }

+

+                MethodInvocation mi2 = mAst.newMethodInvocation();

+                mi2.setName(mAst.newSimpleName("getString"));               //$NON-NLS-1$

+                mi2.setExpression(context);

+                mi2.arguments().add(newNode);

+

+                newNode = mi2;

+                title = "Replace string by Context.getString(R.string...)";

+            }

+

+            TextEditGroup editGroup = new TextEditGroup(title);

+            mEditGroups.add(editGroup);

+            mRewriter.replace(node, newNode, editGroup);

+        }

+        return super.visit(node);

+    }

+

+    /**

+     * Examines if the StringLiteral is part of of an assignment to a string,

+     * e.g. String foo = id.

+     *

+     * The parent fragment is of syntax "var = expr" or "var[] = expr".

+     * We want the type of the variable, which is either held by a

+     * VariableDeclarationStatement ("type [fragment]") or by a

+     * VariableDeclarationExpression. In either case, the type can be an array

+     * but for us all that matters is to know whether the type is an int or

+     * a string.

+     */

+    private boolean examineVariableDeclaration(StringLiteral node) {

+        VariableDeclarationFragment fragment = findParentClass(node,

+                VariableDeclarationFragment.class);

+

+        if (fragment != null) {

+            ASTNode parent = fragment.getParent();

+

+            Type type = null;

+            if (parent instanceof VariableDeclarationStatement) {

+                type = ((VariableDeclarationStatement) parent).getType();

+            } else if (parent instanceof VariableDeclarationExpression) {

+                type = ((VariableDeclarationExpression) parent).getType();

+            }

+

+            if (type instanceof SimpleType) {

+                return isJavaString(type.resolveBinding());

+            }

+        }

+

+        return false;

+    }

+

+    /**

+     * If the expression is part of a method invocation (aka a function call) or a

+     * class instance creation (aka a "new SomeClass" constructor call), we try to

+     * find the type of the argument being used. If it is a String (most likely), we

+     * want to return true (to generate a getString() call). However if there might

+     * be a similar method that takes an int, in which case we don't want to do that.

+     *

+     * This covers the case of Activity.setTitle(int resId) vs setTitle(String str).

+     */

+    @SuppressWarnings("unchecked")  //$NON-NLS-1$

+    private boolean examineMethodInvocation(StringLiteral node) {

+

+        ASTNode parent = null;

+        List arguments = null;

+        IMethodBinding methodBinding = null;

+

+        MethodInvocation invoke = findParentClass(node, MethodInvocation.class);

+        if (invoke != null) {

+            parent = invoke;

+            arguments = invoke.arguments();

+            methodBinding = invoke.resolveMethodBinding();

+        } else {

+            ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class);

+            if (newclass != null) {

+                parent = newclass;

+                arguments = newclass.arguments();

+                methodBinding = newclass.resolveConstructorBinding();

+            }

+        }

+

+        if (parent != null && arguments != null && methodBinding != null) {

+            // We want to know which argument this is.

+            // Walk up the hierarchy again to find the immediate child of the parent,

+            // which should turn out to be one of the invocation arguments.

+            ASTNode child = null;

+            for (ASTNode n = node; n != parent; ) {

+                ASTNode p = n.getParent();

+                if (p == parent) {

+                    child = n;

+                    break;

+                }

+                n = p;

+            }

+            if (child == null) {

+                // This can't happen: a parent of 'node' must be the child of 'parent'.

+                return false;

+            }

+

+            // Find the index

+            int index = 0;

+            for (Object arg : arguments) {

+                if (arg == child) {

+                    break;

+                }

+                index++;

+            }

+

+            if (index == arguments.size()) {

+                // This can't happen: one of the arguments of 'invoke' must be 'child'.

+                return false;

+            }

+

+            // Eventually we want to determine if the parameter is a string type,

+            // in which case a Context.getString() call must be generated.

+            boolean useStringType = false;

+

+            // Find the type of that argument

+            ITypeBinding[] types = methodBinding.getParameterTypes();

+            if (index < types.length) {

+                ITypeBinding type = types[index];

+                useStringType = isJavaString(type);

+            }

+

+            // Now that we know that this method takes a String parameter, can we find

+            // a variant that would accept an int for the same parameter position?

+            if (useStringType) {

+                String name = methodBinding.getName();

+                ITypeBinding clazz = methodBinding.getDeclaringClass();

+                nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) {

+                    if (methodBinding == mb2 || !mb2.getName().equals(name)) {

+                        continue;

+                    }

+                    // We found a method with the same name. We want the same parameters

+                    // except that the one at 'index' must be an int type.

+                    ITypeBinding[] types2 = mb2.getParameterTypes();

+                    int len2 = types2.length;

+                    if (types.length == len2) {

+                        for (int i = 0; i < len2; i++) {

+                            if (i == index) {

+                                ITypeBinding type2 = types2[i];

+                                if (!("int".equals(type2.getQualifiedName()))) {   //$NON-NLS-1$

+                                    // The argument at 'index' is not an int.

+                                    continue nextMethod;

+                                }

+                            } else if (!types[i].equals(types2[i])) {

+                                // One of the other arguments do not match our original method

+                                continue nextMethod;

+                            }

+                        }

+                        // If we got here, we found a perfect match: a method with the same

+                        // arguments except the one at 'index' is an int. In this case we

+                        // don't need to convert our R.id into a string.

+                        useStringType = false;

+                        break;

+                    }

+                }

+            }

+

+            return useStringType;

+        }

+        return false;

+    }

+

+    /**

+     * Examines if the StringLiteral is part of a method declaration (a.k.a. a function

+     * definition) which takes a Context argument.

+     * If such, it returns the name of the variable as a {@link SimpleName}.

+     * Otherwise it returns null.

+     */

+    private SimpleName methodHasContextArgument(StringLiteral node) {

+        MethodDeclaration decl = findParentClass(node, MethodDeclaration.class);

+        if (decl != null) {

+            for (Object obj : decl.parameters()) {

+                if (obj instanceof SingleVariableDeclaration) {

+                    SingleVariableDeclaration var = (SingleVariableDeclaration) obj;

+                    if (isAndroidContext(var.getType())) {

+                        return mAst.newSimpleName(var.getName().getIdentifier());

+                    }

+                }

+            }

+        }

+        return null;

+    }

+

+    /**

+     * Walks up the node hierarchy to find the class (aka type) where this statement

+     * is used and returns true if this class derives from android.content.Context.

+     */

+    private boolean isClassDerivedFromContext(StringLiteral node) {

+        TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);

+        if (clazz != null) {

+            // This is the class that the user is currently writing, so it can't be

+            // a Context by itself, it has to be derived from it.

+            return isAndroidContext(clazz.getSuperclassType());

+        }

+        return false;

+    }

+

+    private Expression findContextFieldOrMethod(StringLiteral node) {

+        TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);

+        ITypeBinding clazzType = clazz == null ? null : clazz.resolveBinding();

+        return findContextFieldOrMethod(clazzType);

+    }

+

+    private Expression findContextFieldOrMethod(ITypeBinding clazzType) {

+        TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>();

+        findContextCandidates(results, clazzType, 0 /*superType*/);

+        if (results.size() > 0) {

+            Integer bestRating = results.keySet().iterator().next();

+            return results.get(bestRating);

+        }

+        return null;

+    }

+

+    /**

+     * Find all method or fields that are candidates for providing a Context.

+     * There can be various choices amongst this class or its super classes.

+     * Sort them by rating in the results map.

+     *

+     * The best ever choice is to find a method with no argument that returns a Context.

+     * The second suitable choice is to find a Context field.

+     * The least desirable choice is to find a method with arguments. It's not really

+     * desirable since we can't generate these arguments automatically.

+     *

+     * Methods and fields from supertypes are ignored if they are private.

+     *

+     * The rating is reversed: the lowest rating integer is used for the best candidate.

+     * Because the superType argument is actually a recursion index, this makes the most

+     * immediate classes more desirable.

+     *

+     * @param results The map that accumulates the rating=>expression results. The lower

+     *                rating number is the best candidate.

+     * @param clazzType The class examined.

+     * @param superType The recursion index.

+     *                  0 for the immediate class, 1 for its super class, etc.

+     */

+    private void findContextCandidates(TreeMap<Integer, Expression> results,

+            ITypeBinding clazzType,

+            int superType) {

+        for (IMethodBinding mb : clazzType.getDeclaredMethods()) {

+            // If we're looking at supertypes, we can't use private methods.

+            if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) {

+                continue;

+            }

+

+            if (isAndroidContext(mb.getReturnType())) {

+                // We found a method that returns something derived from Context.

+

+                int argsLen = mb.getParameterTypes().length;

+                if (argsLen == 0) {

+                    // We'll favor any method that takes no argument,

+                    // That would be the best candidate ever, so we can stop here.

+                    MethodInvocation mi = mAst.newMethodInvocation();

+                    mi.setName(mAst.newSimpleName(mb.getName()));

+                    results.put(Integer.MIN_VALUE, mi);

+                    return;

+                } else {

+                    // A method with arguments isn't as interesting since we wouldn't

+                    // know how to populate such arguments. We'll use it if there are

+                    // no other alternatives. We'll favor the one with the less arguments.

+                    Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen);

+                    if (!results.containsKey(rating)) {

+                        MethodInvocation mi = mAst.newMethodInvocation();

+                        mi.setName(mAst.newSimpleName(mb.getName()));

+                        results.put(rating, mi);

+                    }

+                }

+            }

+        }

+

+        // A direct Context field would be more interesting than a method with

+        // arguments. Try to find one.

+        for (IVariableBinding var : clazzType.getDeclaredFields()) {

+            // If we're looking at supertypes, we can't use private field.

+            if (superType != 0 && Modifier.isPrivate(var.getModifiers())) {

+                continue;

+            }

+

+            if (isAndroidContext(var.getType())) {

+                // We found such a field. Let's use it.

+                Integer rating = Integer.valueOf(superType);

+                results.put(rating, mAst.newSimpleName(var.getName()));

+                break;

+            }

+        }

+

+        // Examine the super class to see if we can locate a better match

+        clazzType = clazzType.getSuperclass();

+        if (clazzType != null) {

+            findContextCandidates(results, clazzType, superType + 1);

+        }

+    }

+

+    /**

+     * Walks up the node hierarchy and returns the first ASTNode of the requested class.

+     * Only look at parents.

+     *

+     * Implementation note: this is a generic method so that it returns the node already

+     * casted to the requested type.

+     */

+    @SuppressWarnings("unchecked")

+    private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) {

+        for (node = node.getParent(); node != null; node = node.getParent()) {

+            if (node.getClass().equals(clazz)) {

+                return (T) node;

+            }

+        }

+        return null;

+    }

+

+    /**

+     * Returns true if the given type is or derives from android.content.Context.

+     */

+    private boolean isAndroidContext(Type type) {

+        if (type != null) {

+            return isAndroidContext(type.resolveBinding());

+        }

+        return false;

+    }

+

+    /**

+     * Returns true if the given type is or derives from android.content.Context.

+     */

+    private boolean isAndroidContext(ITypeBinding type) {

+        for (; type != null; type = type.getSuperclass()) {

+            if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) {

+                return true;

+            }

+        }

+        return false;

+    }

+

+    /**

+     * Returns true if this type binding represents a String or CharSequence type.

+     */

+    private boolean isJavaString(ITypeBinding type) {

+        for (; type != null; type = type.getSuperclass()) {

+            if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) ||

+                CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) {

+                return true;

+            }

+        }

+        return false;

+    }

+}