Mr

 Mr. Rogers' IB Computer Science - Google 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.

 

 

Measuring Acceleration with Android

AccelerometerTest is an Android app that records the acceleration of the phone, using the built-in accelerometer. It shows the instantaneous acceleration for the X, Y, and Z axises, as well as the total acceration. In addition, it allows you to record the acceleration of the phone over a period of time and obtain the highest acceleration in that interval, as well as a graph of the acceleration.

This screenshot shows the default screen of the application. You can see the X, Y, and Z acceleration. T is the total acceleration minus gravity, √X2+Y2+Z2 - g (so it should read 0 when the phone is stationary).
These values can be calibrated to an extent, using the "Calibrate..." option in the application menu, which will allow you to zero the calibration measurement for any of the three axises. The "Reset" option will undo this calibration.
The "Start Recording" button begins the recording of accelerometer data, changing to "Stop Recording" to stop. While recording, the H label displays the highest total acceleration recorded during that session. In addition, if the program records an acceleration of greater than 10g while not recording, it will record the data for one second - useful for recording things such as punches.
To see the recorded data, select the "Switch View" option in the application menu. This will bring up a line graph of the data from the last recording. It also shows the mean acceleration, indicated by a red line. Touching the graph will display a black line where it is touched, labeled to indicate the y-coordinate of the touch relative to the graph. This is useful for measuring extrema. To switch back to the the view, just tap "Switch View" again.

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.Menu;
import android.view.MenuItem;
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
//                      if not already recording.
			((GraphView)findViewById(R.id.graph)).clearPoints();
			timeStart = 0;
			highAccel = 0;
			timer = new MyCountDownTimer(1000, 100);
			timer.start();
		}
	}

	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.menu, menu);
		return true;
	}

	public boolean onOptionsItemSelected(MenuItem item) {
		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
         */
        public void addPoints(List<PointF> p) {
                points.addAll(p);
                sortPoints();
        }
        /**
         * Adds a single point
         * @param p the point to add
         */
        public void addPoint(PointF p) {
                points.add(p);
                sortPoints();
        }
        /**
         * Add a point from x and y coordinates, instead
         * of from a PointF object
         * @param x
         * @param y
         */
        public void addPoint(float x, float y) {
                points.add(new PointF(x, 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>

/res/menu/menu.xml

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"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:title="Calibrate..."
              android:id="@+id/calibrate">
                <menu>
                        <item android:title="X"
                              android:id="@+id/calX"/>
                        <item android:title="Y"
                              android:id="@+id/calY"/>
                        <item android:title="Z"
                              android:id="@+id/calZ"/>
                </menu>
        </item>
        <item android:title="Reset"
              android:id="@+id/reset"/>
        <item android:title="Switch View"
              android:id="@+id/switchView"/>
</menu>