Add support for normalizing virtual methods

This is useful, for example, when comparing the result of deodexing with
the original dex file, to remove the "false" differences caused by the
different potential ways to reference a given virtual method.
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
index 4081a75..6e009fb 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
@@ -366,7 +366,8 @@
     private List<MethodItem> getMethodItems() {
         ArrayList<MethodItem> methodItems = new ArrayList<MethodItem>();
 
-        if ((classDef.options.registerInfo != 0) || (classDef.options.deodex && needsAnalyzed())) {
+        if ((classDef.options.registerInfo != 0) || (classDef.options.normalizeVirtualMethods) ||
+                (classDef.options.deodex && needsAnalyzed())) {
             addAnalyzedInstructionMethodItems(methodItems);
         } else {
             addInstructionMethodItems(methodItems);
@@ -460,7 +461,7 @@
 
     private void addAnalyzedInstructionMethodItems(List<MethodItem> methodItems) {
         MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classDef.options.classPath, method,
-                classDef.options.inlineResolver);
+                classDef.options.inlineResolver, classDef.options.normalizeVirtualMethods);
 
         AnalysisException analysisException = methodAnalyzer.getAnalysisException();
         if (analysisException != null) {
diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmali.java b/baksmali/src/main/java/org/jf/baksmali/baksmali.java
index 47fa406..1e29702 100644
--- a/baksmali/src/main/java/org/jf/baksmali/baksmali.java
+++ b/baksmali/src/main/java/org/jf/baksmali/baksmali.java
@@ -44,19 +44,18 @@
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.DefaultHandler;
 
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
 import java.io.*;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.concurrent.*;
 
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
 public class baksmali {
 
     public static boolean disassembleDexFile(DexFile dexFile, final baksmaliOptions options) {
-        if (options.registerInfo != 0 || options.deodex) {
+        if (options.registerInfo != 0 || options.deodex || options.normalizeVirtualMethods) {
             try {
                 Iterable<String> extraClassPathEntries;
                 if (options.extraClassPathEntries != null) {
diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java b/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java
index 24bf0b9..5dd060f 100644
--- a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java
+++ b/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java
@@ -76,6 +76,7 @@
     public boolean ignoreErrors = false;
     public boolean checkPackagePrivateAccess = false;
     public boolean useImplicitReferences = false;
+    public boolean normalizeVirtualMethods = false;
     public File customInlineDefinitions = null;
     public InlineMethodResolver inlineResolver = null;
     public int registerInfo = 0;
diff --git a/baksmali/src/main/java/org/jf/baksmali/main.java b/baksmali/src/main/java/org/jf/baksmali/main.java
index 7589d3e..17ec383 100644
--- a/baksmali/src/main/java/org/jf/baksmali/main.java
+++ b/baksmali/src/main/java/org/jf/baksmali/main.java
@@ -219,6 +219,9 @@
                 case 'k':
                     options.checkPackagePrivateAccess = true;
                     break;
+                case 'n':
+                    options.normalizeVirtualMethods = true;
+                    break;
                 case 'N':
                     disassemble = false;
                     break;
@@ -282,7 +285,7 @@
             options.deodex = false;
         }
 
-        if (!setBootClassPath && (options.deodex || options.registerInfo != 0)) {
+        if (!setBootClassPath && (options.deodex || options.registerInfo != 0 || options.normalizeVirtualMethods)) {
             if (dexFile instanceof DexBackedOdexFile) {
                 options.bootClassPathEntries = ((DexBackedOdexFile)dexFile).getDependencies();
             } else {
@@ -457,6 +460,10 @@
                         "4.2.1.")
                 .create("k");
 
+        Option normalizeVirtualMethods = OptionBuilder.withLongOpt("normalize-virtual-methods")
+                .withDescription("Normalize virtual method references to the reference the base method.")
+                .create("n");
+
         Option dumpOption = OptionBuilder.withLongOpt("dump-to")
                 .withDescription("dumps the given dex file into a single annotated dump file named FILE" +
                         " (<dexfile>.dump by default), along with the normal disassembly")
@@ -506,6 +513,7 @@
         basicOptions.addOption(noImplicitReferencesOption);
         basicOptions.addOption(dexEntryOption);
         basicOptions.addOption(checkPackagePrivateAccessOption);
+        basicOptions.addOption(normalizeVirtualMethods);
 
         debugOptions.addOption(dumpOption);
         debugOptions.addOption(ignoreErrorsOption);
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedMethodUtil.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedMethodUtil.java
new file mode 100644
index 0000000..775a819
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedMethodUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.analysis;
+
+import org.jf.dexlib2.AccessFlags;
+import org.jf.dexlib2.analysis.util.TypeProtoUtils;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.dexlib2.iface.Method;
+import org.jf.dexlib2.util.MethodUtil;
+import org.jf.dexlib2.util.TypeUtils;
+
+import javax.annotation.Nonnull;
+
+public class AnalyzedMethodUtil {
+    public static boolean canAccess(@Nonnull TypeProto type, @Nonnull Method virtualMethod, boolean checkPackagePrivate,
+                                    boolean checkProtected, boolean checkClass) {
+        if (checkPackagePrivate && MethodUtil.isPackagePrivate(virtualMethod)) {
+            String otherPackage = TypeUtils.getPackage(virtualMethod.getDefiningClass());
+            String thisPackage = TypeUtils.getPackage(type.getType());
+            if (!otherPackage.equals(thisPackage)) {
+                return false;
+            }
+        }
+
+        if (checkProtected && (virtualMethod.getAccessFlags() & AccessFlags.PROTECTED.getValue()) != 0) {
+            if (!TypeProtoUtils.extendsFrom(type, virtualMethod.getDefiningClass())) {
+                return false;
+            }
+        }
+
+        if (checkClass) {
+            ClassPath classPath = type.getClassPath();
+            ClassDef methodClassDef = classPath.getClassDef(virtualMethod.getDefiningClass());
+            if (!TypeUtils.canAccessClass(type.getType(), methodClassDef)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ArrayProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ArrayProto.java
index 8fcfc8c..4aa9a5e 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ArrayProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ArrayProto.java
@@ -32,6 +32,7 @@
 package org.jf.dexlib2.analysis;
 
 import com.google.common.base.Strings;
+import org.jf.dexlib2.iface.Method;
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
 import org.jf.dexlib2.immutable.reference.ImmutableFieldReference;
@@ -160,7 +161,11 @@
 
     @Override
     @Nullable
-    public MethodReference getMethodByVtableIndex(int vtableIndex) {
+    public Method getMethodByVtableIndex(int vtableIndex) {
         return classPath.getClass("Ljava/lang/Object;").getMethodByVtableIndex(vtableIndex);
     }
+
+    @Override public int findMethodIndexInVtable(@Nonnull MethodReference method) {
+        return classPath.getClass("Ljava/lang/Object;").findMethodIndexInVtable(method);
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
index 83104b2..4b8920f 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
@@ -146,7 +146,7 @@
     }
 
     @Nonnull
-    public TypeProto getClass(CharSequence type) {
+    public TypeProto getClass(@Nonnull CharSequence type) {
         return loadedClasses.getUnchecked(type.toString());
     }
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
index d011c1e..d66e8eb 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
@@ -34,7 +34,10 @@
 import com.google.common.base.Predicates;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
-import com.google.common.collect.*;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.primitives.Ints;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.analysis.util.TypeProtoUtils;
@@ -44,6 +47,7 @@
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
 import org.jf.dexlib2.immutable.ImmutableMethod;
+import org.jf.dexlib2.util.MethodUtil;
 import org.jf.util.AlignmentUtils;
 import org.jf.util.ExceptionWithContext;
 import org.jf.util.SparseArray;
@@ -346,7 +350,7 @@
 
     @Override
     @Nullable
-    public MethodReference getMethodByVtableIndex(int vtableIndex) {
+    public Method getMethodByVtableIndex(int vtableIndex) {
         List<Method> vtable = getVtable();
         if (vtableIndex < 0 || vtableIndex >= vtable.size()) {
             return null;
@@ -355,6 +359,20 @@
         return vtable.get(vtableIndex);
     }
 
+    public int findMethodIndexInVtable(@Nonnull MethodReference method) {
+        List<Method> vtable = getVtable();
+        for (int i=0; i<vtable.size(); i++) {
+            Method candidate = vtable.get(i);
+            if (MethodUtil.methodSignaturesMatch(candidate, method)) {
+                if (!classPath.shouldCheckPackagePrivateAccess() ||
+                        AnalyzedMethodUtil.canAccess(this, candidate, true, false, false)) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
     @Nonnull SparseArray<FieldReference> getInstanceFields() {
         if (classPath.isArt) {
             return artInstanceFieldsSupplier.get();
@@ -790,8 +808,9 @@
             outer: for (Method virtualMethod: methods) {
                 for (int i=0; i<vtable.size(); i++) {
                     Method superMethod = vtable.get(i);
-                    if (methodSignaturesMatch(superMethod, virtualMethod)) {
-                        if (!classPath.shouldCheckPackagePrivateAccess() || canAccess(superMethod)) {
+                    if (MethodUtil.methodSignaturesMatch(superMethod, virtualMethod)) {
+                        if (!classPath.shouldCheckPackagePrivateAccess() ||
+                                AnalyzedMethodUtil.canAccess(ClassProto.this, superMethod, true, false, false)) {
                             if (replaceExisting) {
                                 vtable.set(i, virtualMethod);
                             }
@@ -803,37 +822,6 @@
                 vtable.add(virtualMethod);
             }
         }
-
-        private boolean methodSignaturesMatch(@Nonnull Method a, @Nonnull Method b) {
-            return (a.getName().equals(b.getName()) &&
-                    a.getReturnType().equals(b.getReturnType()) &&
-                    a.getParameters().equals(b.getParameters()));
-        }
-
-        private boolean canAccess(@Nonnull Method virtualMethod) {
-            if (!methodIsPackagePrivate(virtualMethod.getAccessFlags())) {
-                return true;
-            }
-
-            String otherPackage = getPackage(virtualMethod.getDefiningClass());
-            String ourPackage = getPackage(getClassDef().getType());
-            return otherPackage.equals(ourPackage);
-        }
-
-        @Nonnull
-        private String getPackage(@Nonnull String classType) {
-            int lastSlash = classType.lastIndexOf('/');
-            if (lastSlash < 0) {
-                return "";
-            }
-            return classType.substring(1, lastSlash);
-        }
-
-        private boolean methodIsPackagePrivate(int accessFlags) {
-            return (accessFlags & (AccessFlags.PRIVATE.getValue() |
-                    AccessFlags.PROTECTED.getValue() |
-                    AccessFlags.PUBLIC.getValue())) == 0;
-        }
     });
 
     private static byte getFieldType(@Nonnull FieldReference field) {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
index 401c0ec..3d0318c 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
@@ -72,6 +72,8 @@
     @Nonnull private final Method method;
     @Nonnull private final MethodImplementation methodImpl;
 
+    private final boolean normalizeVirtualMethods;
+
     private final int paramRegisterCount;
 
     @Nonnull private final ClassPath classPath;
@@ -93,9 +95,10 @@
     private final AnalyzedInstruction startOfMethod;
 
     public MethodAnalyzer(@Nonnull ClassPath classPath, @Nonnull Method method,
-                          @Nullable InlineMethodResolver inlineResolver) {
+                          @Nullable InlineMethodResolver inlineResolver, boolean normalizeVirtualMethods) {
         this.classPath = classPath;
         this.inlineResolver = inlineResolver;
+        this.normalizeVirtualMethods = normalizeVirtualMethods;
 
         this.method = method;
 
@@ -735,21 +738,32 @@
             case SPUT_OBJECT:
                 return true;
             case INVOKE_VIRTUAL:
+                analyzeInvokeVirtual(analyzedInstruction, false);
+                return true;
             case INVOKE_SUPER:
+                analyzeInvokeVirtual(analyzedInstruction, false);
                 return true;
             case INVOKE_DIRECT:
                 analyzeInvokeDirect(analyzedInstruction);
                 return true;
             case INVOKE_STATIC:
+                return true;
             case INVOKE_INTERFACE:
+                // TODO: normalize interfaces
+                return true;
             case INVOKE_VIRTUAL_RANGE:
+                analyzeInvokeVirtual(analyzedInstruction, true);
+                return true;
             case INVOKE_SUPER_RANGE:
+                analyzeInvokeVirtual(analyzedInstruction, true);
                 return true;
             case INVOKE_DIRECT_RANGE:
                 analyzeInvokeDirectRange(analyzedInstruction);
                 return true;
             case INVOKE_STATIC_RANGE:
+                return true;
             case INVOKE_INTERFACE_RANGE:
+                // TODO: normalize interfaces
                 return true;
             case NEG_INT:
             case NOT_INT:
@@ -1545,12 +1559,12 @@
 
         ClassDef thisClass = classPath.getClassDef(method.getDefiningClass());
 
-        if (!canAccessClass(thisClass, classPath.getClassDef(resolvedField.getDefiningClass()))) {
+        if (!TypeUtils.canAccessClass(thisClass.getType(), classPath.getClassDef(resolvedField.getDefiningClass()))) {
 
             // the class is not accessible. So we start looking at objectRegisterTypeProto (which may be different
             // than resolvedField.getDefiningClass()), and walk up the class hierarchy.
             ClassDef fieldClass = classPath.getClassDef(objectRegisterTypeProto.getType());
-            while (!canAccessClass(thisClass, fieldClass)) {
+            while (!TypeUtils.canAccessClass(thisClass.getType(), fieldClass)) {
                 String superclass = fieldClass.getSuperclass();
                 if (superclass == null) {
                     throw new ExceptionWithContext("Couldn't find accessible class while resolving field %s",
@@ -1585,6 +1599,75 @@
         return true;
     }
 
+    private boolean analyzeInvokeVirtual(@Nonnull AnalyzedInstruction analyzedInstruction, boolean isRange) {
+        MethodReference targetMethod;
+
+        if (!normalizeVirtualMethods) {
+            return true;
+        }
+
+        if (isRange) {
+            Instruction3rc instruction = (Instruction3rc)analyzedInstruction.instruction;
+            targetMethod = (MethodReference)instruction.getReference();
+        } else {
+            Instruction35c instruction = (Instruction35c)analyzedInstruction.instruction;
+            targetMethod = (MethodReference)instruction.getReference();
+        }
+
+        TypeProto typeProto = classPath.getClass(targetMethod.getDefiningClass());
+        int methodIndex;
+        try {
+            methodIndex = typeProto.findMethodIndexInVtable(targetMethod);
+        } catch (UnresolvedClassException ex) {
+            return true;
+        }
+
+        if (methodIndex < 0) {
+            return true;
+        }
+
+        Method replacementMethod = typeProto.getMethodByVtableIndex(methodIndex);
+        assert replacementMethod != null;
+        while (true) {
+            String superType = typeProto.getSuperclass();
+            if (superType == null) {
+                break;
+            }
+            typeProto = classPath.getClass(superType);
+            Method resolvedMethod = typeProto.getMethodByVtableIndex(methodIndex);
+            if (resolvedMethod == null) {
+                break;
+            }
+
+            if (!resolvedMethod.equals(replacementMethod)) {
+                if (!AnalyzedMethodUtil.canAccess(typeProto, replacementMethod, true, true, true)) {
+                    continue;
+                }
+
+                replacementMethod = resolvedMethod;
+            }
+        }
+
+        if (replacementMethod.equals(method)) {
+            return true;
+        }
+
+        Instruction deodexedInstruction;
+        if (isRange) {
+            Instruction3rc instruction = (Instruction3rc)analyzedInstruction.instruction;
+            deodexedInstruction = new ImmutableInstruction3rc(instruction.getOpcode(), instruction.getStartRegister(),
+                    instruction.getRegisterCount(), replacementMethod);
+        } else {
+            Instruction35c instruction = (Instruction35c)analyzedInstruction.instruction;
+            deodexedInstruction = new ImmutableInstruction35c(instruction.getOpcode(), instruction.getRegisterCount(),
+                    instruction.getRegisterC(), instruction.getRegisterD(), instruction.getRegisterE(),
+                    instruction.getRegisterF(), instruction.getRegisterG(), replacementMethod);
+        }
+
+        analyzedInstruction.setDeodexedInstruction(deodexedInstruction);
+        return true;
+    }
+
     private boolean analyzeInvokeVirtualQuick(@Nonnull AnalyzedInstruction analyzedInstruction, boolean isSuper,
                                               boolean isRange) {
         int methodIndex;
@@ -1637,12 +1720,13 @@
         // no need to check class access for invoke-super. A class can obviously access its superclass.
         ClassDef thisClass = classPath.getClassDef(method.getDefiningClass());
 
-        if (!isSuper && !canAccessClass(thisClass, classPath.getClassDef(resolvedMethod.getDefiningClass()))) {
+        if (!isSuper && !TypeUtils.canAccessClass(
+                thisClass.getType(), classPath.getClassDef(resolvedMethod.getDefiningClass()))) {
 
             // the class is not accessible. So we start looking at objectRegisterTypeProto (which may be different
             // than resolvedMethod.getDefiningClass()), and walk up the class hierarchy.
             ClassDef methodClass = classPath.getClassDef(objectRegisterTypeProto.getType());
-            while (!canAccessClass(thisClass, methodClass)) {
+            while (!TypeUtils.canAccessClass(thisClass.getType(), methodClass)) {
                 String superclass = methodClass.getSuperclass();
                 if (superclass == null) {
                     throw new ExceptionWithContext("Couldn't find accessible class while resolving method %s",
@@ -1698,24 +1782,6 @@
         return true;
     }
 
-    private boolean canAccessClass(@Nonnull ClassDef accessorClassDef, @Nonnull ClassDef accesseeClassDef) {
-        if (AccessFlags.PUBLIC.isSet(accesseeClassDef.getAccessFlags())) {
-            return true;
-        }
-
-        // Classes can only be public or package private. Any private or protected inner classes are actually
-        // package private.
-        return getPackage(accesseeClassDef.getType()).equals(getPackage(accessorClassDef.getType()));
-    }
-
-    private static String getPackage(String className) {
-        int lastSlash = className.lastIndexOf('/');
-        if (lastSlash < 0) {
-            return "";
-        }
-        return className.substring(1, lastSlash);
-    }
-
     private boolean analyzePutGetVolatile(@Nonnull AnalyzedInstruction analyzedInstruction) {
         return analyzePutGetVolatile(analyzedInstruction, true);
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/PrimitiveProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/PrimitiveProto.java
index 06ab8e1..2c28393 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/PrimitiveProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/PrimitiveProto.java
@@ -31,6 +31,7 @@
 
 package org.jf.dexlib2.analysis;
 
+import org.jf.dexlib2.iface.Method;
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
 import org.jf.util.ExceptionWithContext;
@@ -65,7 +66,11 @@
 
     @Override
     @Nullable
-    public MethodReference getMethodByVtableIndex(int vtableIndex) {
+    public Method getMethodByVtableIndex(int vtableIndex) {
         return null;
     }
+
+    @Override public int findMethodIndexInVtable(@Nonnull MethodReference method) {
+        return -1;
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/TypeProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/TypeProto.java
index f6db239..776363b 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/TypeProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/TypeProto.java
@@ -31,6 +31,7 @@
 
 package org.jf.dexlib2.analysis;
 
+import org.jf.dexlib2.iface.Method;
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
 
@@ -45,5 +46,6 @@
     @Nullable String getSuperclass();
     @Nonnull TypeProto getCommonSuperclass(@Nonnull TypeProto other);
     @Nullable FieldReference getFieldByOffset(int fieldOffset);
-    @Nullable MethodReference getMethodByVtableIndex(int vtableIndex);
+    @Nullable Method getMethodByVtableIndex(int vtableIndex);
+    int findMethodIndexInVtable(@Nonnull MethodReference method);
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/UnknownClassProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/UnknownClassProto.java
index 38256fb..3287345 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/UnknownClassProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/UnknownClassProto.java
@@ -31,6 +31,7 @@
 
 package org.jf.dexlib2.analysis;
 
+import org.jf.dexlib2.iface.Method;
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
 
@@ -75,7 +76,11 @@
 
     @Override
     @Nullable
-    public MethodReference getMethodByVtableIndex(int vtableIndex) {
+    public Method getMethodByVtableIndex(int vtableIndex) {
         return classPath.getClass("Ljava/lang/Object;").getMethodByVtableIndex(vtableIndex);
     }
+
+    @Override public int findMethodIndexInVtable(@Nonnull MethodReference method) {
+        return classPath.getClass("Ljava/lang/Object;").findMethodIndexInVtable(method);
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/util/TypeProtoUtils.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/util/TypeProtoUtils.java
index 0313c7c..e2adf1c 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/util/TypeProtoUtils.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/util/TypeProtoUtils.java
@@ -94,4 +94,16 @@
             return type.getClassPath().getUnknownClass();
         }
     }
+
+    public static boolean extendsFrom(@Nonnull TypeProto candidate, @Nonnull String possibleSuper) {
+        if (candidate.getType().equals(possibleSuper)) {
+            return true;
+        }
+        for (TypeProto superProto: getSuperclassChain(candidate)) {
+            if (superProto.getType().equals(possibleSuper)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/util/MethodUtil.java b/dexlib2/src/main/java/org/jf/dexlib2/util/MethodUtil.java
index 631b892..dc978da 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/util/MethodUtil.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/util/MethodUtil.java
@@ -35,6 +35,7 @@
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.iface.Method;
 import org.jf.dexlib2.iface.reference.MethodReference;
+import org.jf.util.CharSequenceUtils;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -68,6 +69,12 @@
         return methodReference.getName().equals("<init>");
     }
 
+    public static boolean isPackagePrivate(@Nonnull Method method) {
+        return (method.getAccessFlags() & (AccessFlags.PRIVATE.getValue() |
+                AccessFlags.PROTECTED.getValue() |
+                AccessFlags.PUBLIC.getValue())) == 0;
+    }
+
     public static int getParameterRegisterCount(@Nonnull Method method) {
         return getParameterRegisterCount(method, MethodUtil.isStatic(method));
     }
@@ -109,5 +116,11 @@
         return sb.toString();
     }
 
+    public static boolean methodSignaturesMatch(@Nonnull MethodReference a, @Nonnull MethodReference b) {
+        return (a.getName().equals(b.getName()) &&
+                a.getReturnType().equals(b.getReturnType()) &&
+                CharSequenceUtils.listEquals(a.getParameterTypes(), b.getParameterTypes()));
+    }
+
     private MethodUtil() {}
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/util/TypeUtils.java b/dexlib2/src/main/java/org/jf/dexlib2/util/TypeUtils.java
index 02890b8..6be21af 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/util/TypeUtils.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/util/TypeUtils.java
@@ -31,6 +31,8 @@
 
 package org.jf.dexlib2.util;
 
+import org.jf.dexlib2.AccessFlags;
+import org.jf.dexlib2.iface.ClassDef;
 import org.jf.dexlib2.iface.reference.TypeReference;
 
 import javax.annotation.Nonnull;
@@ -49,5 +51,24 @@
         return type.length() == 1;
     }
 
+    @Nonnull
+    public static String getPackage(@Nonnull String type) {
+        int lastSlash = type.lastIndexOf('/');
+        if (lastSlash < 0) {
+            return "";
+        }
+        return type.substring(1, lastSlash);
+    }
+
+    public static boolean canAccessClass(@Nonnull String accessorType, @Nonnull ClassDef accesseeClassDef) {
+        if (AccessFlags.PUBLIC.isSet(accesseeClassDef.getAccessFlags())) {
+            return true;
+        }
+
+        // Classes can only be public or package private. Any private or protected inner classes are actually
+        // package private.
+        return getPackage(accesseeClassDef.getType()).equals(getPackage(accessorType));
+    }
+
     private TypeUtils() {}
 }
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
index 65a82f0..25f7778 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
@@ -71,7 +71,7 @@
         ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
                 15, false);
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
-        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
         Instruction deodexedInstruction = methodAnalyzer.getInstructions().get(0);
         Assert.assertEquals(Opcode.INVOKE_VIRTUAL, deodexedInstruction.getOpcode());
@@ -98,7 +98,7 @@
         ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
                 15, false);
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
-        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
         Instruction deodexedInstruction = methodAnalyzer.getInstructions().get(0);
         Assert.assertEquals(Opcode.INVOKE_STATIC, deodexedInstruction.getOpcode());
@@ -125,7 +125,7 @@
         ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
                 15, false);
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
-        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
         Instruction deodexedInstruction = methodAnalyzer.getInstructions().get(0);
         Assert.assertEquals(Opcode.INVOKE_DIRECT, deodexedInstruction.getOpcode());