How Do I Get Android Crash Logs?

It is a pity that getting crash logs is not that easy for Android as for iOS…

Since Android 2.2 at least there is crash log support for all apps that have been published in the Market Place. Nevertheless, during test & debug prior to publication, crash logs can be very useful.

There’s quite some ideas on the web how to retrieve crash reports, for example by uploading them to your server via php script.
Find some interesting discussions on Stackoverflow discussing the problem.

AndroidBlogger had a much simpler idea: why not just use built-in e-mail to send crash logs directly to your inbox?
Sure, it is not really suited for published apps, but it is more than perfect for pre-release testing, especially because the sending process has to actively be completed by the tester, so he is aware that the developer will be informed about the problem.

The error reporter works in a very simple way:
– the application crashes and is shut down by Android
– on the next start, the error reporter checks if there have been crashes
– if yes, the user will be prompted to send an email with a report (containing, but not limited to, the stack trace)
– user hits send and here we go!

This is what a crash report looks like:
Crashlog Example


New Trace collected :
=====================
Error Report collected on : Mon May 09 19:45:07 GMT+02:00 2011

Informations :
==============

Version : 1.0

Package : YOUR PACKAGE HERE
FilePath : /data/data/de.myapp.app/files
Phone ModelHTC Desire
Android Version : 2.2
Board : bravo
Brand : tmo_uk
Device : bravo
Display : FRF91
Finger Print : tmo_uk/htc_bravo/bravo/bravo:2.2/FRF91/232610:user/release-keys
Host : HPA006
ID : FRF91
Model : HTC Desire
Product : htc_bravo
Tags : release-keys
Time : 1282106362000
Type : user
User : unknown
Total Internal memory : 158072832
Available Internal memory : 125415424
Custom Informations :
=====================
Stack :
=======
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:200)
at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)
at java.util.concurrent.FutureTask.setException(FutureTask.java:124)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)
at java.util.concurrent.FutureTask.run(FutureTask.java:137)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
at java.lang.Thread.run(Thread.java:1102)
Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
at android.graphics.BitmapFactory.nativeDecodeByteArray(Native Method)
at android.graphics.BitmapFactory.decodeByteArray(BitmapFactory.java:401)
at android.graphics.BitmapFactory.decodeByteArray(BitmapFactory.java:414)
at de.myapp.app.utils.AdImageDownloader$BitmapDownloaderTask.doInBackground(AdImageDownloader.java:330)
at de.myapp.app.utils.AdImageDownloader$BitmapDownloaderTask.doInBackground(AdImageDownloader.java:1)
at android.os.AsyncTask$2.call(AsyncTask.java:185)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
… 4 more

Setting up the error reporter is really simple.
Just add the class ErrorReporter.java file into your project. Then, in an appropriate activity, that is shown at the very beginning of the app launch (e.g. splash screen or tab bar activity) post the following code:

ErrorReporter errReporter = new ErrorReporter();
errReporter.Init(this);
errReporter.CheckErrorAndSendMail(this);

And finally here’s the code for the ErrorReporter itself:
ErrorReporter.java

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Random;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Environment;
import android.os.StatFs;

public class ErrorReporter implements Thread.UncaughtExceptionHandler {

private String[] _recipients = new String[] { YOUR EMAIL HERE };
private String _subject = "Crash Report YOUR APP Android";

String VersionName;
String buildNumber;
String PackageName;
String FilePath;
String PhoneModel;
String AndroidVersion;
String Board;
String Brand;
String Device;
String Display;
String FingerPrint;
String Host;
String ID;
String Manufacturer;
String Model;
String Product;
String Tags;
long Time;
String Type;
String User;
HashMap<String, String> CustomParameters = new HashMap<String, String>();

private Thread.UncaughtExceptionHandler PreviousHandler;
private static ErrorReporter S_mInstance;
private Context CurContext;

public void AddCustomData(String Key, String Value) {
CustomParameters.put(Key, Value);
}

private String CreateCustomInfoString() {
String CustomInfo = "";
Iterator iterator = CustomParameters.keySet().iterator();
while (iterator.hasNext()) {
String CurrentKey = (String) iterator.next();
String CurrentVal = CustomParameters.get(CurrentKey);
CustomInfo += CurrentKey + " = " + CurrentVal + "\n";
}
return CustomInfo;
}

static ErrorReporter getInstance() {
if (S_mInstance == null)
S_mInstance = new ErrorReporter();
return S_mInstance;
}

public void Init(Context context) {
PreviousHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
CurContext = context;
}

public long getAvailableInternalMemorySize() {
File path = Environment.getDataDirectory();
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
return availableBlocks * blockSize;
}

public long getTotalInternalMemorySize() {
File path = Environment.getDataDirectory();
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSize();
long totalBlocks = stat.getBlockCount();
return totalBlocks * blockSize;
}

void RecoltInformations(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi;
// Version
pi = pm.getPackageInfo(context.getPackageName(), 0);
VersionName = pi.versionName;
buildNumber = currentVersionNumber(context);
// Package name
PackageName = pi.packageName;

// Device model
PhoneModel = android.os.Build.MODEL;
// Android version
AndroidVersion = android.os.Build.VERSION.RELEASE;

Board = android.os.Build.BOARD;
Brand = android.os.Build.BRAND;
Device = android.os.Build.DEVICE;
Display = android.os.Build.DISPLAY;
FingerPrint = android.os.Build.FINGERPRINT;
Host = android.os.Build.HOST;
ID = android.os.Build.ID;
Model = android.os.Build.MODEL;
Product = android.os.Build.PRODUCT;
Tags = android.os.Build.TAGS;
Time = android.os.Build.TIME;
Type = android.os.Build.TYPE;
User = android.os.Build.USER;

} catch (Exception e) {
e.printStackTrace();
}
}

public String CreateInformationString() {
RecoltInformations(CurContext);

String ReturnVal = "";
ReturnVal += "Version : " + VersionName;
ReturnVal += "\n";
ReturnVal += "Build Number : "+ buildNumber;
ReturnVal += "\n";
ReturnVal += "Package : " + PackageName;
ReturnVal += "\n";
ReturnVal += "FilePath : " + FilePath;
ReturnVal += "\n";
ReturnVal += "Phone Model" + PhoneModel;
ReturnVal += "\n";
ReturnVal += "Android Version : " + AndroidVersion;
ReturnVal += "\n";
ReturnVal += "Board : " + Board;
ReturnVal += "\n";
ReturnVal += "Brand : " + Brand;
ReturnVal += "\n";
ReturnVal += "Device : " + Device;
ReturnVal += "\n";
ReturnVal += "Display : " + Display;
ReturnVal += "\n";
ReturnVal += "Finger Print : " + FingerPrint;
ReturnVal += "\n";
ReturnVal += "Host : " + Host;
ReturnVal += "\n";
ReturnVal += "ID : " + ID;
ReturnVal += "\n";
ReturnVal += "Model : " + Model;
ReturnVal += "\n";
ReturnVal += "Product : " + Product;
ReturnVal += "\n";
ReturnVal += "Tags : " + Tags;
ReturnVal += "\n";
ReturnVal += "Time : " + Time;
ReturnVal += "\n";
ReturnVal += "Type : " + Type;
ReturnVal += "\n";
ReturnVal += "User : " + User;
ReturnVal += "\n";
ReturnVal += "Total Internal memory : " + getTotalInternalMemorySize();
ReturnVal += "\n";
ReturnVal += "Available Internal memory : " + getAvailableInternalMemorySize();
ReturnVal += "\n";

return ReturnVal;
}

public void uncaughtException(Thread t, Throwable e) {
String Report = "";
Date CurDate = new Date();
Report += "Error Report collected on : " + CurDate.toString();
Report += "\n";
Report += "\n";
Report += "Informations :";
Report += "\n";
Report += "==============";
Report += "\n";
Report += "\n";
Report += CreateInformationString();

Report += "Custom Informations :\n";
Report += "=====================\n";
Report += CreateCustomInfoString();

Report += "\n\n";
Report += "Stack : \n";
Report += "======= \n";
final Writer result = new StringWriter();
final PrintWriter printWriter = new PrintWriter(result);
e.printStackTrace(printWriter);
String stacktrace = result.toString();
Report += stacktrace;

Report += "\n";
Report += "Cause : \n";
Report += "======= \n";

// If the exception was thrown in a background thread inside
// AsyncTask, then the actual exception can be found with getCause
Throwable cause = e.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
Report += result.toString();
cause = cause.getCause();
}
printWriter.close();
Report += "**** End of current Report ***";
SaveAsFile(Report);
// SendErrorMail( Report );
PreviousHandler.uncaughtException(t, e);
}

private void SendErrorMail(Context _context, String ErrorContent) {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
String subject = _subject;
String body = "\n\n" + ErrorContent + "\n\n";
sendIntent.putExtra(Intent.EXTRA_EMAIL, _recipients);
sendIntent.putExtra(Intent.EXTRA_TEXT, body);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sendIntent.setType("message/rfc822");
sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
_context.startActivity(Intent.createChooser(sendIntent, "Title:"));
}

private void SaveAsFile(String ErrorContent) {
try {
Random generator = new Random();
int random = generator.nextInt(99999);
String FileName = "stack-" + random + ".stacktrace";
FileOutputStream trace = CurContext.openFileOutput(FileName, Context.MODE_PRIVATE);
trace.write(ErrorContent.getBytes());
trace.close();
} catch (Exception e) {
// ...
}
}

private String[] GetErrorFileList() {
File dir = new File(FilePath + "/");
// Try to create the files folder if it doesn't exist
dir.mkdir();
// Filter for ".stacktrace" files
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".stacktrace");
}
};
return dir.list(filter);
}

private boolean bIsThereAnyErrorFile() {
return GetErrorFileList().length > 0;
}

public void CheckErrorAndSendMail(Context _context) {
try {
FilePath = _context.getFilesDir().getAbsolutePath();
if (bIsThereAnyErrorFile()) {
String WholeErrorText = "";

String[] ErrorFileList = GetErrorFileList();
int curIndex = 0;
final int MaxSendMail = 5;
for (String curString : ErrorFileList) {
if (curIndex++ <= MaxSendMail) {
WholeErrorText += "New Trace collected :\n";
WholeErrorText += "=====================\n ";
String filePath = FilePath + "/" + curString;
BufferedReader input = new BufferedReader(new FileReader(filePath));
String line;
while ((line = input.readLine()) != null) {
WholeErrorText += line + "\n";
}
input.close();
}

// DELETE FILES !!!!
File curFile = new File(FilePath + "/" + curString);
curFile.delete();
}
SendErrorMail(_context, WholeErrorText);
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static String currentVersionNumber(Context a) {
PackageManager pm = a.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo("de.gamedisk.app", PackageManager.GET_SIGNATURES);
return pi.versionName + (pi.versionCode > 0 ? " (" + pi.versionCode + ")" : "");
} catch (NameNotFoundException e) {
return null;
}
}
}