Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat][SDK-347] ANR report #323

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
6 changes: 3 additions & 3 deletions examples/rollbar-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ buildscript {
apply plugin: 'com.android.application'

android {
compileSdkVersion 27
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.rollbar.example.android"
minSdkVersion 16
minSdkVersion 21
// FIXME: Pending further discussion
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 27
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Expand Down
3 changes: 2 additions & 1 deletion examples/rollbar-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
Expand Down
6 changes: 3 additions & 3 deletions rollbar-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ apply from: "$rootDir/gradle/release.gradle"
apply from: "$rootDir/gradle/android.quality.gradle"

android {
compileSdkVersion 27
compileSdkVersion 33
buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+

defaultConfig {
minSdkVersion 16
minSdkVersion 21
// FIXME: Pending further discussion
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 27
targetSdkVersion 33
consumerProguardFiles 'proguard-rules.pro'
manifestPlaceholders = [notifierVersion: VERSION_NAME]
}
Expand Down
14 changes: 14 additions & 0 deletions rollbar-android/src/main/java/com/rollbar/android/Rollbar.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import android.os.Bundle;

import android.util.Log;

import com.rollbar.android.anr.AnrDetector;
import com.rollbar.android.anr.AnrDetectorFactory;
import com.rollbar.android.anr.AnrException;
import com.rollbar.android.notifier.sender.ConnectionAwareSenderFailureStrategy;
import com.rollbar.android.provider.ClientProvider;
import com.rollbar.api.payload.data.TelemetryType;
Expand All @@ -30,6 +34,7 @@
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -164,6 +169,8 @@ public static Rollbar init(Context context, String accessToken, String environme
includeLogcat, provider, DEFAULT_CAPTURE_IP, DEFAULT_MAX_LOGCAT_SIZE,
suspendWhenNetworkIsUnavailable);
}
AnrDetector anrDetector = AnrDetectorFactory.create(context, error -> reportANR(error));
anrDetector.init();
return notifier;
}

Expand Down Expand Up @@ -1086,4 +1093,11 @@ private static void ensureInit(Runnable runnable) {
}
}

private static void reportANR(AnrException error){
Map<String, Object> map = new HashMap<>();
map.put("TYPE", "ANR");
map.put("Threads", error.getThreads());
notifier.log(error, map);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.rollbar.android.anr;

public interface AnrDetector {
void init();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.rollbar.android.anr;

import android.content.Context;
import android.os.Build;

import com.rollbar.android.anr.historical.HistoricalAnrDetector;
import com.rollbar.android.anr.watchdog.WatchdogAnrDetector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AnrDetectorFactory {
private final static Logger LOGGER = LoggerFactory.getLogger(AnrDetectorFactory.class);

public static AnrDetector create(
Context context,
AnrListener anrListener
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
LOGGER.debug("Creating HistoricalAnrDetector");
return new HistoricalAnrDetector(context, anrListener, createHistoricalAnrDetectorLogger());
} else {
LOGGER.debug("Creating WatchdogAnrDetector");
return new WatchdogAnrDetector(context, anrListener);
}
}

private static Logger createHistoricalAnrDetectorLogger() {
return LoggerFactory.getLogger(HistoricalAnrDetector.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.rollbar.android.anr;

import com.rollbar.android.anr.historical.stacktrace.RollbarThread;

import java.util.ArrayList;
import java.util.List;

public final class AnrException extends RuntimeException {

private List<RollbarThread> threads = new ArrayList<>();

public AnrException(String message, Thread thread) {
super(message);
setStackTrace(thread.getStackTrace());
}

public AnrException(StackTraceElement[] mainStackTraceElements, List<RollbarThread> threads) {
super("Application Not Responding");
setStackTrace(mainStackTraceElements);
this.threads = threads;
}

public List<RollbarThread> getThreads() {
return threads;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.rollbar.android.anr;

public interface AnrListener {
/**
* Called when an ANR is detected.
*
* @param error The error describing the ANR.
*/
void onAppNotResponding(AnrException error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.rollbar.android.anr.historical;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.content.Context;

import com.rollbar.android.anr.AnrDetector;
import com.rollbar.android.anr.AnrException;
import com.rollbar.android.anr.AnrListener;
import com.rollbar.android.anr.historical.stacktrace.Lines;
import com.rollbar.android.anr.historical.stacktrace.RollbarThread;
import com.rollbar.android.anr.historical.stacktrace.ThreadParser;

import org.slf4j.Logger;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@SuppressLint("NewApi") // Validated in the Factory
public class HistoricalAnrDetector implements AnrDetector {
private final Logger logger;
private final Context context;
private final AnrListener anrListener;

public HistoricalAnrDetector(
Context context,
AnrListener anrListener,
Logger logger
) {
this.context = context;
this.anrListener = anrListener;
this.logger = logger;
}

@Override
public void init() {
if (anrListener == null) {
logger.error("AnrListener is null");
return;
}
Thread thread = new Thread("HistoricalAnrDetectorThread") {
@Override
public void run() {
super.run();
evaluateLastExitReasons();
}
};
thread.setDaemon(true);
thread.start();
}


private void evaluateLastExitReasons() {
List<ApplicationExitInfo> applicationExitInfoList = getApplicationExitInformation();

if (applicationExitInfoList.isEmpty()) {
logger.debug("Empty ApplicationExitInfo List");
return;
}

for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList) {
if (isNotAnr(applicationExitInfo)) {
continue;
}

try {
List<RollbarThread> threads = getThreads(applicationExitInfo);

if (threads.isEmpty()) {
logger.error("Error parsing ANR");
continue;
}

AnrException anrException = createAnrException(threads);
if (anrException == null) {
logger.error("Main thread not found, skipping ANR");
} else {
anrListener.onAppNotResponding(anrException);
}
} catch (Throwable e) {
logger.error("Can't parse ANR", e);
}
}
}

private boolean isNotAnr(ApplicationExitInfo applicationExitInfo) {
return applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR;
}

private AnrException createAnrException(List<RollbarThread> threads) {
List<RollbarThread> rollbarThreads = new ArrayList<>();
RollbarThread mainThread = null;
for (RollbarThread thread: threads) {
if (thread.isMain()) {
mainThread = thread;
} else {
rollbarThreads.add(thread);
}
}

if (mainThread == null) {
return null;
}
return new AnrException(mainThread.toStackTraceElement(), rollbarThreads);
}

private List<ApplicationExitInfo> getApplicationExitInformation() {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
return activityManager.getHistoricalProcessExitReasons(null, 0, 0);
}

private List<RollbarThread> getThreads(ApplicationExitInfo applicationExitInfo) throws IOException {
Lines lines = getLines(applicationExitInfo);
ThreadParser threadParser = new ThreadParser(isBackground(applicationExitInfo));
return threadParser.parse(lines);
}

private boolean isBackground(ApplicationExitInfo applicationExitInfo) {
return applicationExitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
}

private Lines getLines(ApplicationExitInfo applicationExitInfo) throws IOException {
byte[] dump = getDumpBytes(Objects.requireNonNull(applicationExitInfo.getTraceInputStream()));
return getLines(dump);
}

private Lines getLines(byte[] dump) throws IOException {
return Lines.readLines(toBufferReader(dump));
}

private BufferedReader toBufferReader(byte[] dump) {
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)));
}

private byte[] getDumpBytes(final InputStream trace) throws IOException {
try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {

int nRead;
byte[] data = new byte[1024];

while ((nRead = trace.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}

return buffer.toByteArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.rollbar.android.anr.historical.stacktrace;

public final class Line {
private String text;

public Line(final String text) {
this.text = text;
}

public String getText() {
return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.rollbar.android.anr.historical.stacktrace;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;

public final class Lines {
private final ArrayList<? extends Line> mList;
private final int mMin;
private final int mMax;

/** The read position inside the list. */
public int pos;

/** Read the whole file into a Lines object. */
public static Lines readLines(final BufferedReader in) throws IOException {
final ArrayList<Line> list = new ArrayList<>();

String text;
while ((text = in.readLine()) != null) {
list.add(new Line(text));
}

return new Lines(list);
}

/** Construct with a list of lines. */
public Lines(final ArrayList<Line> list) {
this.mList = list;
mMin = 0;
mMax = mList.size();
}

/** If there are more lines to read within the current range. */
public boolean hasNext() {
return pos < mMax;
}

/**
* Return the next line, or null if there are no more lines to read. Also returns null in the
* error condition where pos is before the beginning.
*/
public Line next() {
if (pos >= mMin && pos < mMax) {
return this.mList.get(pos++);
} else {
return null;
}
}

/** Move the read position back by one line. */
public void rewind() {
pos--;
}
}
Loading
Loading