package com.liato.bankdroid.utils;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import timber.log.Timber;
public class ExceptionUtils {
private static final String PREFIX = "com.liato.bankdroid.";
/**
* Modify an Exception to make it look like it was ultimately caused by Bankdroid.
* <p/>
* The purpose is to make Crashlytics report Urllib exceptions as coming from whatever
* bank Urllib is trying to access.
* <p/>
* For example, this exception:
* <pre>
* java.lang.Exception: This is a test Exception
* at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20)
* at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
* at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
* at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
* at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
* at java.lang.reflect.Method.invoke(Method.java:497)
* at ...
* </pre>
*
* Would be turned into this exception:
* <pre>
* java.lang.Exception: This is a test Exception
* at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20)
* at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
* at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
* at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
* at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
* at java.lang.reflect.Method.invoke(Method.java:497)
* at ...
* Caused by: java.lang.Exception: This is a test Exception
* at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
* at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
* at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
* at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
* at java.lang.reflect.Method.invoke(Method.java:497)
* at ...
* ... 37 more
* </pre>
*/
public static void blameBankdroid(Throwable exception) {
Throwable ultimateCause = getUltimateCause(exception);
if (ultimateCause == null) {
// Unable to find ultimate cause, never mind
return;
}
StackTraceElement[] bankdroidifiedStacktrace =
bankdroidifyStacktrace(ultimateCause.getStackTrace());
if (bankdroidifiedStacktrace.length == 0) {
// No Bankdroid stack frames found, never mind
return;
}
if (bankdroidifiedStacktrace.length == ultimateCause.getStackTrace().length) {
// Bankdroid already to blame, never mind
return;
}
Throwable fakeCause = cloneException(ultimateCause);
if (fakeCause == null) {
Timber.w(new RuntimeException(
"Unable to bankdroidify exception of class: " + ultimateCause.getClass()));
return;
}
// Put the bankdroidified stack trace before the fakeCause's actual stack trace
fakeCause.setStackTrace(concatArrays(bankdroidifiedStacktrace, fakeCause.getStackTrace()));
ultimateCause.initCause(fakeCause);
}
@VisibleForTesting
static StackTraceElement[] concatArrays(StackTraceElement[] a, StackTraceElement[] b) {
StackTraceElement[] returnMe = new StackTraceElement[a.length + b.length];
System.arraycopy(a, 0, returnMe, 0, a.length);
System.arraycopy(b, 0, returnMe, a.length, b.length);
return returnMe;
}
@VisibleForTesting
@Nullable
static Throwable getUltimateCause(Throwable t) {
int laps = 0;
Throwable ultimateCause = t;
while (ultimateCause.getCause() != null) {
ultimateCause = ultimateCause.getCause();
if (laps++ > 10) {
return null;
}
}
return ultimateCause;
}
/**
* Clone message and stacktrace but not the cause.
*/
@Nullable
@VisibleForTesting
static <T extends Throwable> T cloneException(T wrapMe) {
Class<?> newClass = wrapMe.getClass();
while (newClass != null) {
try {
T returnMe =
(T) newClass.getConstructor(String.class).newInstance(wrapMe.getMessage());
returnMe.setStackTrace(wrapMe.getStackTrace());
return returnMe;
} catch (InvocationTargetException e) {
newClass = newClass.getSuperclass();
} catch (NoSuchMethodException e) {
newClass = newClass.getSuperclass();
} catch (InstantiationException e) {
newClass = newClass.getSuperclass();
} catch (IllegalAccessException e) {
newClass = newClass.getSuperclass();
}
}
return null;
}
/**
* Remove all initial non-Bankdroid frames from a stack.
*
* @return A copy of rawStack but with the initial non-Bankdroid frames removed, or null
* if no sensible answer can be given.
*/
@VisibleForTesting
@NonNull
static StackTraceElement[] bankdroidifyStacktrace(final StackTraceElement[] rawStack) {
for (int i = 0; i < rawStack.length; i++) {
StackTraceElement stackTraceElement = rawStack[i];
if (stackTraceElement.getClassName().startsWith(PREFIX)) {
return Arrays.copyOfRange(rawStack, i, rawStack.length);
}
}
// No Bankdroid stack frames found, never mind
return new StackTraceElement[0];
}
}