I just tested out proguard to obfuscate my android app.
But decompiling the apk shows that obfuscated functions have a weird annotation on top of them that displays the real function name, the real class name AND the real file name. How is that possible? Why is this happening?
The decompiled code looks like this,
#f(c = "com.my.app.package.Classname", f = "Classname.kt", l = {49, 51}, m = "RealFunctionName")
public static final class b extends i.b0.j.a.d {
public /* synthetic */ Object s;
public int t;
public final /* synthetic */ Classname u;
public Object v;
public Object w;
public Object x;
/* JADX INFO: super call moved to the top of the method (can break code semantics) */
public b(Classname classname, i.b0.d dVar) {
super(dVar);
this.u = classname;
}
#Override // i.b0.j.a.a
public final Object l(Object obj) {
this.s = obj;
this.t |= Integer.MIN_VALUE;
return this.u.c(this);
}
}
This happens even if I remove ALL proguard rules to make it obfuscate everything without any exceptions. So where does this annotation come from? And why does it happen? Any help would be appreciated!
Java compiler does not preserve parameter names for any interface, unless newer compiler option -parameter is used (I am not sure how to use it with android studio) - refer example below.
Since java compiler does not preserve parameters names, Android Studio "code -> implement methods" is not able to generate code with original parameter names.
The question is, how to implement a library module so that Android Studio Menu, Code->Implement Methods correctly generates code with all the original parameter names.
For example, following is a simple class and an interface. This class is in a separate aar module. When application uses this AAR, implements TablaListener and asks AndroidStudio to generate interface methods stubs, the parameter names are not preserved.
Please note that proguard is NOT used.
Any ideas?
public class TablaCore {
public interface TablaListener {
/**
* #param params
* #param data
* #return
*/
boolean TablaCore_onAction(String params, byte[] data);
}
private static TablaListener mListener = null;
public static void setListener(TablaListener myListener) {
mListener = myListener;
}
public TablaListener getListener() {
return mListener;
}
}
It is easy to demonstrate by compiling and decompiling above class. This is decompiled version
public class TablaCore
{
private static TablaListener mListener = null;
public static void setListener(TablaListener myListener)
{
mListener = myListener;
}
public TablaListener getListener()
{
return mListener;
}
public static abstract interface TablaListener
{
public abstract boolean TablaCore_onAction(String paramMessageParams, byte[] paramArrayOfByte);
}
}
You have to include Android SDK source code.
Go to: File > Settings... > Apperance & Behavior > System Settings > Android SDK
On the tab SDK Platforms select Show Package Details and find and select appropriate Sources for Android XX. Unfortunately, currently for Android API 27, there is no sources, but there is for Sources for Android 26. Apply changes - the download window should appear automatically.
After downloading and restarting implementing methods should use proper names for method parameters.
I'm following this tutorial and this Custom Detector Example in order to implement Custom Lint Rules. Basically what I've done is:
Create a new Android Project in Android Studio;
Create a java module for project created in step 1;
On module's build.gradle, import Lint API dependencies;
Create an Issue & IssueRegistry & CustomDetector;
Reference the IssueRegistry on module's build.gradle;
Create Unit tests;
My problem is, during the execution of my JUnits, I always receive "No Warning". When I debug the test, I can see that my Custom Detector isn't called, what am I doing wrong?
Strings.java
public class Strings {
public static final String STR_ISSUE_001_ID = "VarsMustHaveMoreThanOneCharacter";
public static final String STR_ISSUE_001_DESCRIPTION = "Avoid naming variables with only one character";
public static final String STR_ISSUE_001_EXPLANATION = "Variables named with only one character do not pass any meaning to the reader. " +
"Variables name should clear indicate the meaning of the value it is holding";
}
Issues.java
public class Issues {
public static final
Issue ISSUE_001 = Issue.create(
STR_ISSUE_001_ID,
STR_ISSUE_001_DESCRIPTION,
STR_ISSUE_001_EXPLANATION,
SECURITY,
// Priority ranging from 0 to 10 in severeness
6,
WARNING,
new Implementation(VariableNameDetector.class, ALL_RESOURCES_SCOPE)
);
}
IssuesRegistry.java
public class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
#Override
public List<Issue> getIssues() {
List<Issue> issues = new ArrayList<>();
issues.add(ISSUE_001);
return issues;
}
}
VariableNameDetector.java
public class VariableNameDetector extends Detector implements Detector.JavaScanner {
public VariableNameDetector() {
}
#Override
public boolean appliesToResourceRefs() {
return false;
}
#Override
public boolean appliesTo(Context context, File file) {
return true;
}
#Override
#Nullable
public AstVisitor createJavaVisitor(JavaContext context) {
return new NamingConventionVisitor(context);
}
#Override
public List<String> getApplicableMethodNames() {
return null;
}
#Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
List<Class<? extends Node>> types = new ArrayList<>(1);
types.add(lombok.ast.VariableDeclaration.class);
return types;
}
#Override
public void visitMethod(
JavaContext context,
AstVisitor visitor,
MethodInvocation methodInvocation
) {
}
#Override
public void visitResourceReference(
JavaContext context,
AstVisitor visitor,
Node node,
String type,
String name,
boolean isFramework
) {
}
private class NamingConventionVisitor extends ForwardingAstVisitor {
private final JavaContext context;
NamingConventionVisitor(JavaContext context) {
this.context = context;
}
#Override
public boolean visitVariableDeclaration(VariableDeclaration node) {
StrictListAccessor<VariableDefinitionEntry, VariableDeclaration> varDefinitions =
node.getVariableDefinitionEntries();
for (VariableDefinitionEntry varDefinition : varDefinitions) {
String name = varDefinition.astName().astValue();
if (name.length() == 1) {
context.report(
ISSUE_001,
context.getLocation(node),
STR_ISSUE_001_DESCRIPTION
);
return true;
}
}
return false;
}
}
}
build.gradle
apply plugin: 'java'
configurations {
lintChecks
}
ext {
VERSION_LINT_API = '24.3.1'
VERSION_LINT_API_TESTS = '24.3.1'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "com.android.tools.lint:lint-api:$VERSION_LINT_API"
implementation "com.android.tools.lint:lint-checks:$VERSION_LINT_API"
testImplementation "com.android.tools.lint:lint-tests:$VERSION_LINT_API_TESTS"
}
jar {
manifest {
attributes('Lint-Registry': 'br.com.edsilfer.lint_rules.resources.IssueRegistry')
}
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
TestVariableNameDetector.java
private static final String ARG_DEFAULT_LINT_SUCCESS_LOG = "No warnings.";
#Override
protected Detector getDetector() {
return new VariableNameDetector();
}
#Override
protected List<Issue> getIssues() {
return Collections.singletonList(Issues.ISSUE_001);
}
public void test_file_with_no_variables_with_length_equals_01() throws Exception {
assertEquals(
ARG_DEFAULT_LINT_SUCCESS_LOG,
lintProject(java("assets/Test.java", "public class Test {public String sampleVariable;}"))
);
}
public void test_file_with_variables_with_length_equals_01() throws Exception {
assertEquals(
ARG_DEFAULT_LINT_SUCCESS_LOG,
lintProject(java("assets/Test3.java", "public class Test {public String a;bnvhgvhj}"))
);
}
}
P.S.: on Java's module I do not have access to assetsor res folder, that is the reason why I've created a String.java and I'm using java(to, source) in my Unit test - I assumed that this java method does the same as the xml from the tutorial link I referenced at the top of this question.
It turned out that in my case the problem was with the JUnit itself. I think that the way I was attempting to simulate the file was wrong. The text below is part of the README.md of a sample project that I've created in order to document what I've learned from this API and answers the question in the title:
Create
Create a new Android Project;
Create a new Java Library Module - Custom Lint Rules are packaged into .jar libraries once they are ready, therefore the easiest way to implement them using them is inside a Java Module Library;
On module's build.gradle:
add target and source compatibility to Java 1.7;
add dependencies for lint-api, lint-checks and test dependencies;
add jar packing task containing two attributes: Manifest-Version and Lint-Registry, set the first to 1.0 and the second as the full path to a class that will later on contain the issue's catalog;
add a default tasl assemble;
[OPTIONAL]: add a task that will copy the generated .jar into ~/.android/lint;
Check REF001 and choose a Detector that best suits your needs, based on it create and implement a class to fulfill the Detector's role;
Still based on REF0001 chosen file, create and implement a Checker class, later referring to it inside Detector's createJavaVisitor() method;
for the sake of SRP, do not place Checker in the same file of Detector's class;
Copy the generated .jar file from build/lib to ~/.android/lint - if you added a task on build.gradle that does this you can skip this step;
Restart the computer - once created and moved into ~/.android/lint, the Custom Rules should be read by Lint next time the program starts. In order to have the alert boxes inside Android Studio, it is enough to invalidate caches and restart the IDE, however, to have your custom rules caught on Lint Report when ./gradlew check, it might be necessary to restart your computer;
Testing Detectors and Checkers
Testing Custom Rules is not an easy task to do - mainly due the lack of documentation for official APIs. This section will present two approaches for dealing with this. The main goal of this project is to create custom rules that will be run against real files, therefore, test files will be necessary for testing them. They can be places in src/test/resources folder from your Lint Java Library Module;
Approach 01: LintDetectorTest
Make sure you've added all test dependencies - checkout sample project's build.gradle;
Copy EnhancedLintDetectorTest.java and FileUtils.java into your project's test directory;
There is a known bug with Android Studio that prevents it from seeing files from src/test/resources folder, these files are a workaround for that;
EnhancedLintDetectorTest.java should return all issues that will be subject of tests. A nice way to do so is getting them from Issue Registry;
Create a test class that extends from EnhancedLintDetectorTest.java;
Implement getDetector() method returning an instance of the Detector to be tested;
Use lintFiles("test file path taking resources dir as root") to perform the check of the Custom Rules and use its result object to assert the tests;
Note that LintDetectorTest.java derives from TestCase.java, therefore, you're limited to JUnit 3.
Approach 02: Lint JUnit Rule
You might have noticed that Approach 01 might be a little overcomplicated, despite the fact that you're limited to JUnit 3 features. Because of that GitHub user a11n created a Lint JUnit Rule that allows the test of Custom Lint Rules in a easier way that counts with JUnit 4 and up features. Please, refer to his project README.md for details about how to create tests using this apprach.
Currently, Lint JUnit Rule do not correct the root dir for test files and you might no be able to see the tests passing from the IDE - however it works when test are run from command line. An issue and PR were created in order to fix this bug.
I'm not sure how to use the AST Api, however I'm personally using Psi and this is one of my lint checks that are working for me.
public final class RxJava2MethodCheckReturnValueDetector extends Detector implements Detector.JavaPsiScanner {
static final Issue ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE =
Issue.create("MethodMissingCheckReturnValue", "Method is missing the #CheckReturnValue annotation",
"Methods returning RxJava Reactive Types should be annotated with the #CheckReturnValue annotation.",
MESSAGES, 8, WARNING,
new Implementation(RxJava2MethodCheckReturnValueDetector.class, EnumSet.of(JAVA_FILE, TEST_SOURCES)));
#Override public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>singletonList(PsiMethod.class);
}
#Override public JavaElementVisitor createPsiVisitor(#NonNull final JavaContext context) {
return new CheckReturnValueVisitor(context);
}
static class CheckReturnValueVisitor extends JavaElementVisitor {
private final JavaContext context;
CheckReturnValueVisitor(final JavaContext context) {
this.context = context;
}
#Override public void visitMethod(final PsiMethod method) {
final PsiType returnType = method.getReturnType();
if (returnType != null && Utils.isRxJava2TypeThatRequiresCheckReturnValueAnnotation(returnType)) {
final PsiAnnotation[] annotations = method.getModifierList().getAnnotations();
for (final PsiAnnotation annotation : annotations) {
if ("io.reactivex.annotations.CheckReturnValue".equals(annotation.getQualifiedName())) {
return;
}
}
final boolean isMethodMissingCheckReturnValueSuppressed = context.getDriver().isSuppressed(context, ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE, method);
if (!isMethodMissingCheckReturnValueSuppressed) {
context.report(ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE, context.getLocation(method.getNameIdentifier()), "Method should have #CheckReturnValue annotation");
}
}
}
}
}
Checkout the many more I wrote here.
Very strange error. I'm trying to implement a simple interface in a non-activity class within my android application. However, I get a run-time error. If I remove the implementation, the code works fine. I will let the code speak for itself, feel free to chime in. Thanks!
This throws a ClassNotFoundException when instantiating an instance of Globals
public class Globals implements IReceivable {
private volatile static Globals uniqueGlobal;
private static ClientSocket clientSocket;
private Globals()
{
}
public static Globals getInstance() {
synchronized (Globals.class) {
if (uniqueGlobal == null) {
uniqueGlobal = new Globals();
uniqueGlobal.InitializeClient();
}
}
return uniqueGlobal;
}
#Override
public void MessageReceived(UUID clientId, String message) {
// TODO Auto-generated method stub
}
}
When I remove it, everything works just fine
public class Globals {
private volatile static Globals uniqueGlobal;
private static ClientSocket clientSocket;
private Globals()
{
}
public static Globals getInstance() {
synchronized (Globals.class) {
if (uniqueGlobal == null) {
uniqueGlobal = new Globals();
uniqueGlobal.InitializeClient();
}
}
return uniqueGlobal;
}
}
EDIT
Here is the IReceivable interface
public interface IReceivable {
public void MessageReceived(UUID clientId, String message);
}
And the exact error:
12-02 10:14:15.859: E/AndroidRuntime(546):
java.lang.NoClassDefFoundError: com.example.utilities.Globals
The line that is throwing the error looks like this:
Globals global = Globals.getInstance();
Ok, solved the problem. What I didn't tell you is that the interface is within a jar file that I have added to my project.
I added the jar file to my project and correctly included it into the build path, however, I did not add the jar file to the 'libs' folder. Apparently, android at runtime will only include jar files that are within this folder.
Good learning experience for me. Thanks!
Still being relatively green to things, I'm trying to figure out if it is possible to write JUnit functional tests against a prebuilt app sans source. I've found a means of re-signing the app but I'd like to know how to include it in an Eclipse project as a dependency of my tests?
Yes, this is indeed possible using Java reflections.
Besides this, it can be necessary to resign the app you want to test. Download it from your device (e.g., using the app AppMonster), resign it using the resign.jar (http://www.troido.de/re-sign.jar) and install it again using adb.
Afterwards, everything should work fine.
#SuppressWarnings("rawtypes")
public class YourTestSuite extends ActivityInstrumentationTestCase2 {
/** Package ID of the app under test. */
private static final String TARGET_PACKAGE_ID = "com.something";
/** ID of the app under test's main activity. */
private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME = "com.something.HomeActivity";
/** Launcher activity class. */
private static Class<?> launcherActivityClass;
/** Static code to find the activity class. */
static {
try {
launcherActivityClass = Class
.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* Constructor
*
* #throws ClassNotFoundException
*/
#SuppressWarnings("unchecked")
public YourTestSuite() throws ClassNotFoundException {
super(TARGET_PACKAGE_ID, launcherActivityClass);
}
/* Your test code comes here. */
}