Sunday, November 7, 2010

Adding Crash Report

Cross-posted (in Portuguese) at Technè - Blog de Tecnologia do C.E.S.A.R.

Using only Twitter API for Java ME it is possible to add Crash Report to a Java ME application.

Crash Report is a set of information with details of an issue that made an application stop working.
Operating Systems like Microsoft Windows, Mac OS and Linux distros have this feature.
We browsers like Mozilla Firefox and Google Chrome also do. Android too since version 2.2 .

It is recommended that the application has its own Twitter account. This way all development team can follow application reports.

What should go inside a Crash Report?


  1. Application name and version: MIDlet-Name e Version

  2. Name and value of attributes and variables

  3. Exception that caused the issue

  4. Handset model running the app: microedition.platform



Below is a class that will handle all this info:

public class CrashReport {
private StringBuffer report = new StringBuffer();
private CrashReport (String content) {
report.append(content);
}
public CrashReport (MIDlet midlet, String details, Throwable e) {
if (midlet != null) {
report.append(midlet.getAppProperty("MIDlet-Name"));
report.append(' ');
report.append(midlet.getAppProperty("MIDlet-Version"));
report.append(' ');
}
if (details != null) {
report.append(details).append(' ');
}
if (e != null) {
report.append(e).append(' ');
}
String platform = System.getProperty("microedition.platform");
if (platform != null) {
report.append(platform);
}
}
public String toString () {
return report.toString();
}
public byte [] toByteArray () {
return toString().getBytes();
}
public static CrashReport fromByteArray (byte[] data) {
return new CrashReport(new String(data));
}
}


As soon as we get an unexpected exception we show a dialog stating that there was an error as asking if the details can be sent to the development team. If the user answers no, the report is discarded. If the answer is yes, the app tries to send the message right away.

Below is the class responsible to send the report:


public class CrashReportTwitterSender {
private TweetER tweetEr;
public CrashReportTwitterSender () throws IOException {
// check previous post on how to get a credential instance
UserAccountManager accountManager = UserAccountManager.getInstance(credential);
try {
if (accountManager.verifyCredential()) {
tweetEr = TweetER.getInstance(accountManager);
} else {
throw new IOException("Could not verify credential");
}
} catch (LimitExceededException ex) {
throw new IOException("caused by " + ex.getClass().getName());
}
}
public boolean sendReport (CrashReport report) {
try {
tweetEr.post(new Tweet(report.toString()));
return true;
} catch (Exception ex) {
return false;
}
}
}


If an error happens when the app is trying to send the report we ask the user to save it and send later.
If the user answers no, the report is discarded. If the answer is yes, the app saves the report for future usage.

Below is the class responsible to save CrashReport instances at RecordStore:


public class CrashReportStore {
private RecordStore reports;
public CrashReportStore () throws IOException {
try {
reports = RecordStore.openRecordStore("reports", true);
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex.getClass().getName());
}
}
public void addReport (CrashReport report) throws IOException {
byte [] data = report.toByteArray();
try {
reports.addRecord(data, 0, data.length);
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex);
}
}
public boolean hasReports () {
try {
return this.reports.getNumRecords() > 0;
} catch (RecordStoreNotOpenException ex) {
return false;
}
}
public void close() throws IOException {
try {
this.reports.closeRecordStore();
} catch (RecordStoreException ex) {
throw new IOException("caused by " + ex);
}
}
public void sendAllReports (CrashReportTwitterSender sender) throws IOException {
try {
RecordEnumeration re = this.reports.enumerateRecords(null, null, false);
while (re.hasNextElement()) {
sendReportAndDeleteRecord(sender, re.nextRecordId());
}
} catch (Exception ex) {
throw new IOException("caused by " + ex);
}
}
private void sendReportAndDeleteRecord (CrashReportTwitterSender sender, int id)
throws RecordStoreException
{
byte [] data = this.reports.getRecord(id);
CrashReport report = CrashReport.fromByteArray(data);
if (sender.sendReport(report)) {
this.reports.deleteRecord(id);
}
}
}


Next time the application is started it can check if CrashReportStore.hasReports().
If there is none we do not need to bother. If there is some, we ask the user to send the reports now or later.

Below is a sample code to test what we wrote here. It should be pasted at some class that extends MIDlet.


CrashReport report = new CrashReport(this, "crash report test", null);
CrashReportTwitterSender sender = new CrashReportTwitterSender();
if (sender.sendReport(report) == false) {
CrashReportStore store = new CrashReportStore();
store.addReport(report);
store.close();
}


Related topics:

Friday, October 22, 2010

Saturday, September 25, 2010

Nine Slice Scalling

Below is the public contract (constructor and methods) of my NineSliceImage component. It may be used to draw Buttons, Title bars and Frames.

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

public class NineSliceImage {

public NineSliceImage (Image img, int vMargin, int hMargin) { ... }

public void paintFrameAround (Graphics g, int x, int y, int width, int height) { ... }
public Image createFrameFor (int width, int height) { ... }

}

And below is a sample usage:

class MyClass extends Canvas {
MyClass () throws java.io.IOException {
Image i = Image.createImage("/pattern.PNG");
this.nsi = new NineSliceImage(i, 5, 5);
}
protected void paint(Graphics g) {
nsi.paintFrameAround(g, 20, 20, 20, 20);
}
}

Where patter is the following image:

And the resulting frame is:

Full component source code below:

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

public class NineSliceImage {

private Image originalImage;
private int verticalMargin;
private int horizontalMargin;
int middlePieceHeight;
int middlePieceWidth;

public NineSliceImage (Image img, int vMargin, int hMargin) {
validateConstructorParameters(img, vMargin, hMargin);
setAttributesValuesFor(img, vMargin, hMargin);
}

public Image createFrameFor (int width, int height) {
validateWidthAndHeight(width, height);

int middleHeight = getMiddleHeight(height);
int middleWidth = getMiddleWidth(width);
Image frame = Image.createImage(middleWidth + (2 * this.horizontalMargin),
middleHeight + (2 * this.verticalMargin));
Graphics g = frame.getGraphics();

this.paintFrameAround(g, horizontalMargin, verticalMargin, middleWidth, middleHeight);

return frame;
}

public void paintFrameAround (Graphics g, int x, int y, int width, int height) {
validateWidthAndHeight(width, height);

int middleWidth = getMiddleWidth(width);
int middleHeight = getMiddleWidth(height);
int totalWidth = middleWidth + (2 * this.horizontalMargin);
int totalHeight = middleHeight + (2 * this.verticalMargin);
x = x - ((middleWidth - width) / 2) - this.horizontalMargin;
y = y - ((middleHeight - height) / 2) - this.verticalMargin;

drawFrameCorners(g, x, y, middleWidth, middleHeight, totalWidth, totalHeight);
drawTopAndBottomLines(g, x, y, width, totalHeight);
drawLeftAndRightColumns(g, x, y, height, totalWidth);
}

private void validateConstructorParameters (Image img, int vMargin, int hMargin) {
if (img == null || vMargin <= 0 || hMargin <= 0) {
throw new IllegalArgumentException();
}
if (vMargin >= img.getHeight() / 2) {
throw new IllegalArgumentException();
}
if (hMargin >= img.getWidth() / 2) {
throw new IllegalArgumentException();
}
}

private void setAttributesValuesFor (Image img, int vMargin, int hMargin) {
this.originalImage = img;
this.verticalMargin = vMargin;
this.horizontalMargin = hMargin;
this.middlePieceHeight = this.originalImage.getHeight()
- (2 * this.verticalMargin);
this.middlePieceWidth = this.originalImage.getWidth()
- (2 * this.horizontalMargin);
}

private void validateWidthAndHeight (int width, int height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException();
}
}

private int getMiddleWidth (int width) {
return getHorizontalRepetitions(width) * middlePieceWidth;
}

private int getHorizontalRepetitions (int width) {
int horizontalRepetitions = width / middlePieceWidth;

if (width % middlePieceWidth > 0) {
horizontalRepetitions++;
}

return horizontalRepetitions;
}

private int getMiddleHeight (int height) {
return getVerticalRepetitions(height) * middlePieceHeight;
}

private int getVerticalRepetitions (int height) {
int verticalRepetitions = height / middlePieceHeight;

if (height % middlePieceHeight > 0) {
verticalRepetitions++;
}

return verticalRepetitions;
}

private void drawFrameCorners (Graphics g, int x, int y, int middleWidth,
int middleHeight, int totalWidth, int totalHeight) {
g.setClip(x, y,
this.horizontalMargin, this.verticalMargin);
g.drawImage(this.originalImage, x, y,
Graphics.LEFT | Graphics.TOP);
g.setClip(x, y + this.verticalMargin + middleHeight,
this.horizontalMargin, this.verticalMargin);
g.drawImage(this.originalImage, x, y + totalHeight,
Graphics.LEFT | Graphics.BOTTOM);
g.setClip(x + this.horizontalMargin + middleWidth, y,
this.horizontalMargin, this.verticalMargin);
g.drawImage(this.originalImage, x + totalWidth, y,
Graphics.RIGHT | Graphics.TOP);
g.setClip(x + this.horizontalMargin + middleWidth, y + this.verticalMargin + middleHeight,
this.horizontalMargin, this.verticalMargin);
g.drawImage(this.originalImage, x + totalWidth, y + totalHeight,
Graphics.RIGHT | Graphics.BOTTOM);
}

private void drawTopAndBottomLines (Graphics g, int x, int y, int width, int totalHeight) {
int horizontalRepetitions = getHorizontalRepetitions(width);
x += horizontalMargin;
for (int i = 0; i < horizontalRepetitions; i++) {
g.setClip(x, y, middlePieceWidth, this.verticalMargin);
g.drawImage(this.originalImage, x - this.horizontalMargin, y,
Graphics.LEFT | Graphics.TOP);
g.setClip(x, y + totalHeight - this.verticalMargin,
middlePieceWidth, this.verticalMargin);
g.drawImage(this.originalImage, x - this.horizontalMargin,
y + totalHeight - this.verticalMargin,
Graphics.LEFT | Graphics.TOP);
x += middlePieceWidth;
}
}

private void drawLeftAndRightColumns (Graphics g, int x, int y, int height, int totalWidth) {
int verticalRepetitions = getVerticalRepetitions(height);

y += this.verticalMargin;
for (int i = 0; i < verticalRepetitions; i++) {
g.setClip(x, y, this.horizontalMargin, middlePieceHeight);
g.drawImage(this.originalImage, x, y - this.verticalMargin,
Graphics.LEFT | Graphics.TOP);
g.setClip(x + totalWidth - this.horizontalMargin, y,
this.horizontalMargin, middlePieceHeight);
g.drawImage(this.originalImage, x + totalWidth - this.horizontalMargin,
y - this.verticalMargin,
Graphics.LEFT | Graphics.TOP);
y += middlePieceHeight;
}
}

}