It would be great to get a detailed report every time your app crashes.
And that’s exactly what we’re gonna do in this post.
I’l be using ACRA, a wonderfully simple crash reporting library.Its also well documented.
A brief intro into ACRA :
1. It is capable of creating detailed reports on every app crash.
2. To obtain the report, ACRA can be configured in the following ways:
- Send it to various supported backends.
- Send it to your custom backend.
- Send it via email.
- Implement your own report handler.
Well, it already has an option to email the report !! Then why this post ?
Because for emailing, ACRA prompts the user to manually choose the emailing app (The intent chooser basically). This is simply an overhead.
So here’s what we’re gonna do.
We implement our own custom report handler.
This handler will capture the report and email it silently without the user knowing about it.
This method must be employed only during the testing phase of the app. Emailing silently from a production app will attract a lot of angry users, plus legal issues.
Let’s do some code :).
A brief overview of the classes used:
1. ACRAReportSender – Our custom report handler.
2. MyApplication – Our application class (to initialize ACRA).
3. MainActivity – Our launcher activity (to test the report sender).
4. GMailSender , JSSEProvider – Helper classes for ACRAReportSender.
You’ll need the following jars :
Here are the classes :
The application class – To configure ACRA.
import org.acra.ACRA; import org.acra.annotation.ReportsCrashes; import android.app.Application; @ReportsCrashes(formKey = "qwertysvvasdfghjkl1235547657891234") public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); ACRA.init(this); // instantiate the report sender with the email credentials. // these will be used to send the crash report ACRAReportSender reportSender = new ACRAReportSender("yourEmail@gmail.com", "yourPassword"); // register it with ACRA. ACRA.getErrorReporter().setReportSender(reportSender); } }
Our custom report handler.
import org.acra.ReportField; import org.acra.collector.CrashReportData; import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderException; import android.content.Context; import android.util.Log; public class ACRAReportSender implements ReportSender { private String emailUsername ; private String emailPassword ; public ACRAReportSender(String emailUsername, String emailPassword) { super(); this.emailUsername = emailUsername; this.emailPassword = emailPassword; } @Override public void send(Context context, CrashReportData report) throws ReportSenderException { // Extract the required data out of the crash report. String reportBody = createCrashReport(report); // instantiate the email sender GMailSender gMailSender = new GMailSender(emailUsername, emailPassword); try { // specify your recipients and send the email gMailSender.sendMail("CRASH REPORT", reportBody, emailUsername, "recipient1@gmail.com, recipient2@gmail.com"); } catch (Exception e) { Log.d("Error Sending email", e.toString()); } } /** Extract the required data out of the crash report.*/ private String createCrashReport(CrashReportData report) { // I've extracted only basic information. // U can add loads more data using the enum ReportField. See below. StringBuilder body = new StringBuilder(); body .append("Device : " + report.getProperty(ReportField.BRAND) + "-" + report.getProperty(ReportField.PHONE_MODEL)) .append("\n") .append("Android Version :" + report.getProperty(ReportField.ANDROID_VERSION)) .append("\n") .append("App Version : " + report.getProperty(ReportField.APP_VERSION_CODE)) .append("\n") .append("STACK TRACE : \n" + report.getProperty(ReportField.STACK_TRACE)); return body.toString(); } }
I’ve extracted only the basic data required. For a complete list of available data, refer this.
Helper for class for sending the email.
import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Security; import java.util.Properties; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.mail.Message; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import android.util.Log; public class GMailSender extends javax.mail.Authenticator { private String mailhost = "smtp.gmail.com"; private String user; private String password; private Session session; static { Security.addProvider(new JSSEProvider()); } public GMailSender(String user, String password) { this.user = user; this.password = password; Properties props = new Properties(); props.setProperty("mail.transport.protocol", "smtp"); props.setProperty("mail.host", mailhost); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.port", "465"); props.put("mail.smtp.socketFactory.port", "465"); props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); props.put("mail.smtp.socketFactory.fallback", "false"); props.setProperty("mail.smtp.quitwait", "false"); session = Session.getDefaultInstance(props, this); } protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, password); } public synchronized void sendMail(String subject, String body, String sender, String recipients) throws Exception { try{ MimeMessage message = new MimeMessage(session); DataHandler handler = new DataHandler(new ByteArrayDataSource(body.getBytes(), "text/plain")); message.setSender(new InternetAddress(sender)); message.setSubject(subject); message.setDataHandler(handler); if (recipients.indexOf(',') > 0) message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipients)); else message.setRecipient(Message.RecipientType.TO, new InternetAddress(recipients)); Transport.send(message); }catch(Exception e){ Log.d("Error Sendin Email", e.toString()); } } public class ByteArrayDataSource implements DataSource { private byte[] data; private String type; public ByteArrayDataSource(byte[] data, String type) { super(); this.data = data; this.type = type; } public ByteArrayDataSource(byte[] data) { super(); this.data = data; } public void setType(String type) { this.type = type; } public String getContentType() { if (type == null) return "application/octet-stream"; else return type; } public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(data); } public String getName() { return "ByteArrayDataSource"; } public OutputStream getOutputStream() throws IOException { throw new IOException("Not Supported"); } } }
Another helper class
import java.security.AccessController; import java.security.Provider; @SuppressWarnings("serial") public final class JSSEProvider extends Provider { public JSSEProvider() { super("HarmonyJSSE", 1.0, "Harmony JSSE Provider"); AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() { public Void run() { put("SSLContext.TLS", "org.apache.harmony.xnet.provider.jsse.SSLContextImpl"); put("Alg.Alias.SSLContext.TLSv1", "TLS"); put("KeyManagerFactory.X509", "org.apache.harmony.xnet.provider.jsse.KeyManagerFactoryImpl"); put("TrustManagerFactory.X509", "org.apache.harmony.xnet.provider.jsse.TrustManagerFactoryImpl"); return null; } }); } }
Finally , The launcher activity to test this out.