Mr
 Mr. Rogers' IB Computer Science -- Android SmartPhone Programming Project Help boost K-12 science, math, and computer education. Donate your obsolete Android phone to us (see below)! We promise to put it to good use in a classroom.

 Home Projects 2010-2011    Level  Greenville, SC

## Measuring Acceleration with Android

### AccelerometerTest

Status

Completely functional, but lacking some polish

Purpose

To enable the recording and viewing of acceleration over time

Features
• Records acceleration on the X, Y, and Z axises, as well as total acceleration.
• Displays instantaneous X, Y, Z, and total acceleration, as well as the highest recorded total acceleration.
• The accerometer can be zero-calibrated via a menu option
• Graphs the total acceleration on a acceleration vs. time graph, also displays the mean acceleration
• The graph can be touched to display the acceleration at the y-value of the touch
Potential Features
• A more effective method of calibration could be used
• The phone could relay acceleration data to a PC program for more advanced analysis and visualization of data, storage of data, and viewing multiple sets of data from multiple phones simultaneously
• Some of the aforementioned features could also be added to the phone application
• The graph could be optimized for better display on different devices
Instructions

When the AccelerometerTest application is opened, it will display the X, Y, and Z acceleration, and total (T) acceleration, which is equal to √X2+Y2+Z2 - g (so it should read close to zero when the phone is stationary). To begin recording acceleration, press the "Start Recording" button; press it again to stop. Alternately, if the phone undergoes an acceleration of more than 10g while not recording, it will record acceleration for the next second (good for measuring the force of punches). While recording, the H label will display the highest total acceleration recorded thus far in that recording. The application can hold/view one recording at a time.

The accelerometer can be calibrated using the "Calibrate..." option in the application menu. Selected, this will bring up another menu from which one can choose the calibrate either the X, Y, or Z axises. The selected axis will be zero-calibrated, i.e. adjusted so that the acceleration reads zero at the current acceleration of the phone on that axis. It unfortunately will not read *exactly* zero, due to the limitations of the accelerometer itself. The calibration of the axises can be reset by selecting "Reset" from the application menu.

The third option on the application menu, "Switch View", will switch between the initial view and the graph view. The graph view displays an acceleration vs. time graph for the last acceleration recording. The horizontal red line on the graph marks the mean acceleration. When touched, the graph will display a horizontal black line and label that marks the y (acceleration) value at that point. This is useful for determining acceleration values that aren't easy to measure with the labels on the edges of the graph.

## Class AccelerometerTest

Every Android app has at least one Activity class, i.e. a class that extends android.app.Activity. One of those activity classes is started when the application is launched. In this application, the Activity class is AccelerometerTest. It handles application lifecycle events (onCreate(), onStop(), etc.) as well as creating and managing menus, among other things. In this case, android.hardware.SensorEventListener is implemented so the application can also recieve sensor events - such as changes in acceleration from the accelerometer.
```package com.test.accel;

import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.FloatMath;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import android.widget.ViewFlipper;

/**
* The primary class for this application
* @author Stephan Williams
*
*/
public class AccelerometerTest extends Activity implements SensorEventListener {

//      These are the values used for calibration.
private float dx = 0;
private float dy = 0;
private float dz = 0;

//      Keeps track of the recording start times so
//      the origin of the graph can be kept at t=0
private long timeStart = 0;

//      holds the last sensor event, used for calibration
SensorEvent lastEvent;

//      This is my subclass of CountDownTimer, which adds some convenience
//      methods for checking if the timer is finished
MyCountDownTimer timer = new MyCountDownTimer(0, 0);

private float highAccel = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);

//      Sets up this class (which implements SensorEventListener) to recieve
//      sensor events, specifically from the accelerometer.
SensorManager manager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
Sensor accel = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
manager.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME);

//      Sets up the actions for the "Start Recording" button
((Button)findViewById(R.id.startTimer)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//                              When clicked, if the timer is not running (meaning the app is not recording),
//                              clear the graph, change the button text to "Stop Recording", and reset the
//                              timer and highest acceleration values.
if (!timer.isRunning()) {
((GraphView)findViewById(R.id.graph)).clearPoints();
((Button)findViewById(R.id.startTimer)).setText(R.string.stopTimer);
timeStart = 0;
highAccel = 0;
timer = new MyCountDownTimer(Long.MAX_VALUE, 100);
timer.start();
} else {
((Button)findViewById(R.id.startTimer)).setText(R.string.startTimer);
timer.cancelTimer();
}
}
});
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {

}

public void onStop() {
super.onStop();
((Button)findViewById(R.id.startTimer)).setText(R.string.startTimer);
timer.cancelTimer();
}

@Override
public void onSensorChanged(SensorEvent event) {
lastEvent = event;
TextView xAccel = ((TextView)findViewById(R.id.x_accel));
TextView yAccel = ((TextView)findViewById(R.id.y_accel));
TextView zAccel = ((TextView)findViewById(R.id.z_accel));
TextView tAccel = ((TextView)findViewById(R.id.all_accel));
TextView hAccel = ((TextView)findViewById(R.id.high_accel));

xAccel.setText("X: " + (event.values[0] - dx));
yAccel.setText("Y: " + (event.values[1] - dy));
zAccel.setText("Z: " + (event.values[2] - dz));

float totalAccel = FloatMath.sqrt((event.values[0] - dx) * (event.values[0] - dx) +
(event.values[1] - dy) * (event.values[1] - dy) +
(event.values[2] - dz) * (event.values[2] - dz)) - SensorManager.GRAVITY_EARTH;

tAccel.setText("T: " + totalAccel);
if (timer.isRunning()) {
//                      sets the start time of recording
if (timeStart == 0) timeStart = event.timestamp;
if (totalAccel > highAccel) highAccel = totalAccel;
hAccel.setText("H: " + highAccel);
//                      add the point to the graph, converting the nanoseconds from the timer to seconds
((GraphView)findViewById(R.id.graph)).addPoint((float)((event.timestamp - timeStart) / 1.0E9), totalAccel);
} else if (totalAccel > 10) {
//                      for the "punch detector" feature, records high accelerations
((GraphView)findViewById(R.id.graph)).clearPoints();
timeStart = 0;
highAccel = 0;
timer = new MyCountDownTimer(1000, 100);
timer.start();
}
}

return true;
}

switch (item.getItemId()) {
//              gets which menu item was pressed to calibrate,
//              uses the last recorded accelerometer value to zero
//              the display value
case (R.id.calX):
dx = lastEvent.values[0];
break;
case (R.id.calY):
dy = lastEvent.values[1];
break;
case (R.id.calZ):
dz = lastEvent.values[2];
break;
//              reset and view switching buttons
case (R.id.reset):
dx = dy = dz = 0;
break;
case (R.id.switchView):
((ViewFlipper)findViewById(R.id.details)).showNext();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
}
```

## Class GraphView

A class extending android.view.View, the base class for UI components. Displays a graph. The graph is currently not resolution independent, though it would definitely need to be made so should this app ever be released to the Android market.
Making this class was the first time I can remember having to optimize a program for speed issues; the graph originally had a ~1 second lag redrawing. It turned out that the slowdown was caused by String.format(), which I had been using to constrain the labels to a certain number of decimal places. This was eventually fixed by simply rounding the number down a few decimal places (see the round() method at the end), which had a similar effect, and was substantially faster.
package com.test.accel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnTouchListener;

/**
* A basic graph view. Can be set to either fit the graph bounds to
* all the given points, or use user-defined bounds. It displays the
* mean of the points, as well as
* @author Stephan Williams
*
*/
public class GraphView extends View implements OnTouchListener {
private List<PointF> points;
private float yInt;
private float xInt;
private RectF window;
private boolean fitToScreen;
private float touchAt;

public GraphView(Context context, AttributeSet attrs) {
super(context, attrs);
this.setOnTouchListener(this);
points = new ArrayList<PointF>();
yInt = 1;
xInt = 1;
window = new RectF(-10, -10, 10, 10);
fitToScreen = true;
touchAt = 1000;
}
@Override
public void onDraw(Canvas canvas) {
Paint white = new Paint() {{ setColor(0xFFFFFFFF); setTextSize(15); setAntiAlias(true); }};
Paint gray = new Paint() {{ setColor(0xFFDDDDDD); }};
Paint redAA = new Paint() {{ setColor(0xFFFF0000); setTextSize(15); setAntiAlias(true); }};
Paint blackAA = new Paint() {{ setColor(0xFF000000); setTextSize(15); setAntiAlias(true); }};
Paint red = new Paint() {{ setColor(0xFFFF0000); setTextSize(15); }};
Paint black = new Paint() {{ setColor(0xFF000000); setTextSize(15); }};

//              bounds represents the coordinate system of the graph,
//              clip represents the graph relative to the screen.
RectF bounds = fitToScreen ? createBoundsRect(points, 1) : window;
RectF clip = new RectF(50, 50, 320, 455);

canvas.drawRect(clip, white);
//              prevent NullPointerExceptions
if (points.size() == 0) return;

//              draw y axis labels
canvas.rotate(90, 160, 160);
for (float i = Math.min(bounds.top, bounds.bottom);
i <= Math.max(bounds.top, bounds.bottom);
i += fitToScreen ? (Math.abs(bounds.top - bounds.bottom) / 15f) : Math.abs(yInt)) {
float off = mapPoint(new PointF(0, i), bounds, clip).x;
canvas.drawText("" + round(i), 10, 325 - off, white);
canvas.drawLine(48, 320 - off, 455, 320 - off, gray);
}

canvas.rotate(-90, 160, 160);

//              draw x axis labels
for (float i = Math.min(bounds.left, bounds.right);
i <= Math.max(bounds.left, bounds.right);
i += fitToScreen ? (Math.abs(bounds.left - bounds.right) / 25) : Math.abs(xInt)) {
float off = mapPoint(new PointF(i, 0), bounds, clip).y;
canvas.drawText("" + round(i), 10, off + 5, white);
canvas.drawLine(48, off, 320, off, gray);
}

canvas.clipRect(clip);
//              draw mean
float temp = mapPoint(new PointF(0, getAverageY()), bounds, clip).x;
canvas.drawLine((int)temp, clip.top, (int)temp, clip.bottom, red);
canvas.rotate(90, 160, 160);
canvas.drawText("" + round(getAverageY()), clip.top + 2, clip.right - temp - 2, redAA);
canvas.rotate(-90, 160, 160);

//              draw graph
for (int i = 1; i < points.size(); i++) {
PointF p1 = mapPoint(points.get(i - 1), bounds, clip);
PointF p2 = mapPoint(points.get(i), bounds, clip);
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, new Paint() {{ setColor(0xFF0000FF); }});
}

//              draw trace line
temp = mapPoint(new PointF(touchAt, 0), clip, bounds).y;
canvas.drawLine(touchAt, clip.top, touchAt, clip.bottom, black);
canvas.rotate(90, 160, 160);
canvas.drawText(String.format("%3.2f", temp), clip.top + 2, clip.right - touchAt - 2, blackAA);
canvas.rotate(-90, 160, 160);

}
/**
* Replaces the existing points in the graph with
* ones from a list
* @param p the list of points
*/
public void setPoints(List<PointF> p) {
points = p;
sortPoints();
}
/**
* Adds a list of points to the existing points
* @param p this list of points to add
*/
sortPoints();
}
/**
* @param p the point to add
*/
sortPoints();
}
/**
* of from a PointF object
* @param x
* @param y
*/
public void addPoint(float x, float y) {
sortPoints();
}
/**
* Removes all points from the graph
*/
public void clearPoints() {
points.clear();
}
/**
* Sets the user-defined x interval for the graph.
* @see #setFitToScreen(boolean)
* @param xint
*/
public void setXInterval(float xint) {
xInt = Math.abs(xint);
}
/**
* @see #setXInterval(float)
* @see #setFitToScreen(boolean)
* @return the user-defined x interval for the graph
*/
public float getXInterval() {
return xInt;
}
/**
* Sets the user-defined y interval for the graph.
* @see #setFitToScreen(boolean)
* @param yint
*/
public void setYInterval(float yint) {
yInt = Math.abs(yint);
}
/**
* @see #setYInterval(float)
* @see #setFitToScreen(boolean)
* @return the user-defined y interval for the graph
*/
public float getYInterval() {
return yInt;
}
/**
* Sorts the list of points in ascending order, by x value
*/
private void sortPoints() {
Collections.sort(points, new Comparator<PointF>() {
@Override
public int compare(PointF p1, PointF p2) {
return Float.compare(p1.x, p2.x);
}
});
}
/**
* Creates a rectangle that completely encloses all points
* in the given list, with the specified border around them.
* The border is taken as the same units as the graph.
* @param points a list of points
* @param border the width of the border to put around the points
* @return a rectangle enclosing all given points
*/
private RectF createBoundsRect(List<PointF> points, float border) {
if (points.size() == 0) return new RectF();
float ymin = points.get(0).y;
float ymax = points.get(0).y;
for (PointF p : points) {
if (p.y < ymin) ymin = p.y;
if (p.y > ymax) ymax = p.y;
}
return new RectF(points.get(0).x - border,
ymin - 2 *border,
points.get(points.size() - 1).x + border,
ymax + 2 * border);
}
/**
* Maps a point from one rectangle to another, so it appears in
* the same relative spot
* @param p the point to map
* @param srcRect
* @param destRect
* @return the mapped point
*/
private PointF mapPoint(PointF p, RectF srcRect, RectF destRect) {
return new PointF((p.y - srcRect.top) / (srcRect.bottom - srcRect.top) *
(destRect.right - destRect.left) + destRect.left,
(p.x - srcRect.left) / (srcRect.right - srcRect.left) *
(destRect.bottom - destRect.top) + destRect.top);
}
/**
*
* @return the average y value of the points in the graph
*/
public float getAverageY() {
if (points.size() == 0) return 0;
float temp = 0;
for (PointF pf : points) temp += pf.y;
return temp / points.size();
}
/**
* Sets whether the graph should fit all the points on the
* graph, or use the user-defined bounds and intervals.
* @param fit
*/
public void setFitToScreen(boolean fit) {
fitToScreen = fit;
}
/**
* @see #setFitToScreen(boolean)
* @return
*/
public boolean getFitToScreen() {
return fitToScreen;
}
/**
* Sets the user-defined viewing window for the graph.
* @see #setFitToScreen(boolean)
* @param rect
*/
public void setWindow(RectF rect) {
window = rect;
window.sort();
}
/**
* @see #setFitToScreen(boolean)
* @see #setWindow(RectF)
* @return the user-defined window for the graph
*/
public RectF getWindow() {
return window;
}
@Override
public boolean onTouch(View view, MotionEvent e) {
touchAt = e.getX();
postInvalidate();

return true;
}
private float round(float f) {
return (int)(f * 100f) / 100f;
}
}

## Class MyCountDownTimer

Because of the way this program is structured in AccelerationTest, a timer was needed that could be asked whether or not it had finished. Because no such timer with that functionality existed in the Android API, I subclassed the closest available - android.os.CountDownTimer - and added a few simple methods.
```package com.test.accel;

import android.os.CountDownTimer;
import android.widget.ViewFlipper;
/**
* Subclass of CountDownTimer with methods added for
* telling if the timer has stopped.
* @author Stephan Williams
*
*/
public class MyCountDownTimer extends CountDownTimer {

private boolean running = false;

public MyCountDownTimer(long millisInFuture, long countDownInterval) {
super(millisInFuture, countDownInterval);
}

@Override
public void onFinish() {
running = false;
}

@Override
public void onTick(long millisUntilFinished) {
running = true;
}

public boolean isRunning() {
return running;
}

public void cancelTimer() {
running = false;
cancel();
}

}
```

## /res/layout/main.xml

Although UIs in Android apps can be created programmatically, it is considered better practice to design them using XML, where they can then be referenced (and instantiated) using the autogenerated resource class, R. This xml file lays out the entire UI for this program (sans menu), using a ViewFlipper to switch between the primary and graph views.
```<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ViewFlipper android:id="@+id/details"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20pt"
android:id="@+id/x_accel"
android:singleLine="true"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20pt"
android:id="@+id/y_accel"
android:singleLine="true"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20pt"
android:id="@+id/z_accel"
android:singleLine="true"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20pt"
android:id="@+id/all_accel"
android:singleLine="true"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20pt"
android:id="@+id/high_accel"
android:singleLine="true"
android:text="H:"
/>
<Button
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/startTimer"
android:text="@string/startTimer"
android:textSize="14pt"
/>
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.test.accel.GraphView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/graph"
/>
</LinearLayout>
</ViewFlipper>
</LinearLayout>
```

Like the rest of the UI, the application menu can also be created with XML, with the code behind it going in the Activity class.
```<?xml version="1.0" encoding="utf-8"?>
<item android:title="Calibrate..."
android:id="@+id/calibrate">
<item android:title="X"
android:id="@+id/calX"/>
<item android:title="Y"
android:id="@+id/calY"/>
<item android:title="Z"
android:id="@+id/calZ"/>