blob: 901161956ed020529844a113883826ff427c9bc5 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.sensors;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.JsonWriter;
import android.util.Log;
import android.view.Window;
import android.widget.ImageView;
import android.widget.Toast;
import com.android.cts.verifier.R;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
// ----------------------------------------------------------------------
/**
* An activity that does recording of the camera video and rotation vector data at the same time.
*/
public class RVCVRecordActivity extends Activity {
private static final String TAG = "RVCVRecordActivity";
//private static final boolean LOCAL_LOGD = true;
private static final boolean LOCAL_LOGV = false;
private MotionIndicatorView mIndicatorView;
private SoundPool mSoundPool;
private int [] mSoundPoolLookup;
private File mRecordDir;
private RecordProcedureController mController;
private VideoRecorder mVideoRecorder;
private RVSensorLogger mRVSensorLogger;
private CoverageManager mCoverManager;
private CameraPreviewer mPreviewer;
public static final int AXIS_NONE = 0;
public static final int AXIS_ALL = SensorManager.AXIS_X +
SensorManager.AXIS_Y +
SensorManager.AXIS_Z;
// For Rotation Vector algorithm research use
private final static boolean LOG_RAW_SENSORS = false;
private RawSensorLogger mRawSensorLogger;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Hide the window title.
requestWindowFeature(Window.FEATURE_NO_TITLE);
// inflate xml
setContentView(R.layout.cam_preview_overlay);
// locate views
mIndicatorView = (MotionIndicatorView) findViewById(R.id.cam_indicator);
initStoragePath();
}
@Override
protected void onPause() {
super.onPause();
mController.quit();
mPreviewer.end();
endSoundPool();
}
@Override
protected void onResume() {
super.onResume();
// delay the initialization as much as possible
init();
}
/** display toast message
*
* @param msg Message content
*/
private void message(String msg) {
Context context = getApplicationContext();
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, msg, duration);
toast.show();
}
/**
* Initialize components
*
*/
private void init() {
mPreviewer = new CameraPreviewer();
mPreviewer.init();
mCoverManager = new CoverageManager();
mIndicatorView.setDataProvider(
mCoverManager.getAxis(SensorManager.AXIS_X),
mCoverManager.getAxis(SensorManager.AXIS_Y),
mCoverManager.getAxis(SensorManager.AXIS_Z) );
initSoundPool();
mRVSensorLogger = new RVSensorLogger(this);
mVideoRecorder = new VideoRecorder(mPreviewer.getCamera());
if (LOG_RAW_SENSORS) {
mRawSensorLogger = new RawSensorLogger(mRecordDir);
}
mController = new RecordProcedureController(this);
}
/**
* Notify recording is completed. This is the successful exit.
*/
public void notifyComplete() {
message("Capture completed!");
Uri resultUri = Uri.fromFile(mRecordDir);
Intent result = new Intent();
result.setData(resultUri);
setResult(Activity.RESULT_OK, result);
finish();
}
/**
* Notify the user what to do next in text
*
* @param axis SensorManager.AXIS_X or SensorManager.AXIS_Y or SensorManager.AXIS_Z
*/
private void notifyPrompt(int axis) {
// It is not XYZ because of earlier design have different definition of
// X and Y
final String axisName = "YXZ";
message("Manipulate the device in " + axisName.charAt(axis-1) + " axis (as illustrated) about the pattern.");
}
/**
* Ask indicator view to redraw
*/
private void redrawIndicator() {
mIndicatorView.invalidate();
}
/**
* Switch to a different axis for display and logging
* @param axis
*/
private void switchAxis(int axis) {
ImageView imageView = (ImageView) findViewById(R.id.cam_overlay);
final int [] prompts = {R.drawable.prompt_x, R.drawable.prompt_y, R.drawable.prompt_z};
if (axis >=SensorManager.AXIS_X && axis <=SensorManager.AXIS_Z) {
imageView.setImageResource(prompts[axis-1]);
mIndicatorView.enableAxis(axis);
mRVSensorLogger.updateRegister(mCoverManager.getAxis(axis), axis);
notifyPrompt(axis);
} else {
imageView.setImageDrawable(null);
mIndicatorView.enableAxis(AXIS_NONE);
}
redrawIndicator();
}
/**
* Asynchronized way to call switchAxis. Use this if caller is not on UI thread.
* @param axis @param axis SensorManager.AXIS_X or SensorManager.AXIS_Y or SensorManager.AXIS_Z
*/
public void switchAxisAsync(int axis) {
// intended to be called from a non-UI thread
final int fAxis = axis;
runOnUiThread(new Runnable() {
public void run() {
// UI code goes here
switchAxis(fAxis);
}
});
}
/**
* Initialize sound pool for user notification
*/
private void initSoundPool() {
final int MAX_STREAM = 10;
int i=0;
mSoundPool = new SoundPool(MAX_STREAM, AudioManager.STREAM_MUSIC, 0);
mSoundPoolLookup = new int[MAX_STREAM];
// TODO: add different sound into this
mSoundPoolLookup[i++] = mSoundPool.load(this, R.raw.next_axis, 1);
mSoundPoolLookup[i++] = mSoundPool.load(this, R.raw.next_axis, 1);
mSoundPoolLookup[i++] = mSoundPool.load(this, R.raw.next_axis, 1);
}
private void endSoundPool() {
mSoundPool.release();
}
/**
* Play notify sound to user
* @param id ID of the sound to be played
*/
public void playNotifySound(int id) {
mSoundPool.play(mSoundPoolLookup[id], 1, 1, 0, 0, 1);
}
/**
* Start the sensor recording
*/
public void startRecordSensor() {
mRVSensorLogger.init();
if (LOG_RAW_SENSORS) {
mRawSensorLogger.init();
}
}
/**
* Stop the sensor recording
*/
public void stopRecordSensor() {
mRVSensorLogger.end();
if (LOG_RAW_SENSORS) {
mRawSensorLogger.end();
}
}
/**
* Start video recording
*/
public void startRecordVideo() {
mVideoRecorder.init();
}
/**
* Stop video recording
*/
public void stopRecordVideo() {
mVideoRecorder.end();
}
/**
* Wait until a sensor recording for a certain axis is fully covered
* @param axis
*/
public void waitUntilCovered(int axis) {
mCoverManager.waitUntilCovered(axis);
}
/**
*
*/
private void initStoragePath() {
File rxcvRecDataDir = new File(Environment.getExternalStorageDirectory(),"RVCVRecData");
// Create the storage directory if it does not exist
if (! rxcvRecDataDir.exists()) {
if (! rxcvRecDataDir.mkdirs()) {
Log.e(TAG, "failed to create main data directory");
}
}
mRecordDir = new File(rxcvRecDataDir, new SimpleDateFormat("yyMMdd-hhmmss").format(new Date()));
if (! mRecordDir.mkdirs()) {
Log.e(TAG, "failed to create rec data directory");
}
}
/**
* Get the sensor log file path
* @return Path of the sensor log file
*/
public String getSensorLogFilePath() {
return new File(mRecordDir, "sensor.log").getPath();
}
/**
* Get the video recording file path
* @return Path of the video recording file
*/
public String getVideoRecFilePath() {
return new File(mRecordDir, "video.mp4").getPath();
}
/**
* Write out important camera/video information to a JSON file
* @param width width of frame
* @param height height of frame
* @param frameRate frame rate in fps
* @param fovW field of view in width direction
* @param fovH field of view in height direction
*/
public void writeVideoMetaInfo(int width, int height, float frameRate, float fovW, float fovH) {
try {
JsonWriter writer =
new JsonWriter(
new OutputStreamWriter(
new FileOutputStream(
new File(mRecordDir, "videometa.json").getPath()
)
)
);
writer.beginObject();
writer.name("fovW").value(fovW);
writer.name("fovH").value(fovH);
writer.name("width").value(width);
writer.name("height").value(height);
writer.name("frameRate").value(frameRate);
writer.endObject();
writer.close();
}catch (FileNotFoundException e) {
// Not very likely to happen
e.printStackTrace();
}catch (IOException e) {
// do nothing
e.printStackTrace();
Log.e(TAG, "Writing video meta data failed.");
}
}
/**
* Camera preview control class
*/
class CameraPreviewer {
private Camera mCamera;
CameraPreviewer() {
try {
mCamera = Camera.open(); // attempt to get a default Camera instance
}
catch (Exception e) {
// Camera is not available (in use or does not exist)
Log.e(TAG, "Cannot obtain Camera!");
}
}
/**
* Get the camera to be previewed
* @return Reference to Camera used
*/
public Camera getCamera() {
return mCamera;
}
/**
* Setup the camera
*/
public void init() {
if (mCamera != null) {
double alpha = mCamera.getParameters().getHorizontalViewAngle()*Math.PI/180.0;
int width = 1920;
double fx = width/2/Math.tan(alpha/2.0);
if (LOCAL_LOGV) Log.v(TAG, "View angle="
+ mCamera.getParameters().getHorizontalViewAngle() +" Estimated fx = "+fx);
RVCVCameraPreview cameraPreview =
(RVCVCameraPreview) findViewById(R.id.cam_preview);
cameraPreview.init(mCamera);
} else {
message("Cannot open camera!");
finish();
}
}
/**
* End the camera preview
*/
public void end() {
if (mCamera != null) {
mCamera.release(); // release the camera for other applications
mCamera = null;
}
}
}
/**
* Manage a set of RangeCoveredRegister objects
*/
class CoverageManager {
// settings
private final int MAX_TILT_ANGLE = 60; // +/- 60
//private final int REQUIRED_TILT_ANGLE = 50; // +/- 50
private final int TILT_ANGLE_STEP = 5; // 5 degree(s) per step
private final int YAW_ANGLE_STEP = 10; // 10 degree(s) per step
RangeCoveredRegister[] mAxisCovered;
CoverageManager() {
mAxisCovered = new RangeCoveredRegister[3];
// X AXIS
mAxisCovered[0] = new RangeCoveredRegister(-MAX_TILT_ANGLE, +MAX_TILT_ANGLE, TILT_ANGLE_STEP);
// Y AXIS
mAxisCovered[1] = new RangeCoveredRegister(-MAX_TILT_ANGLE, +MAX_TILT_ANGLE, TILT_ANGLE_STEP);
// Z AXIS
mAxisCovered[2] = new RangeCoveredRegister(YAW_ANGLE_STEP);
}
public RangeCoveredRegister getAxis(int axis) {
// SensorManager.AXIS_X = 1, need offset -1 for mAxisCovered array
return mAxisCovered[axis-1];
}
public void waitUntilCovered(int axis) {
// SensorManager.AXIS_X = 1, need offset -1 for mAxisCovered array
while(!mAxisCovered[axis-1].isFullyCovered()) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
if (LOCAL_LOGV) {
Log.v(TAG, "waitUntilCovered axis = "+ axis + " is interrupted");
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* A class controls the video recording
*/
class VideoRecorder
{
private MediaRecorder mRecorder;
private Camera mCamera;
private boolean mRunning = false;
private int [] mPreferredProfiles = { CamcorderProfile.QUALITY_480P, // smaller -> faster
CamcorderProfile.QUALITY_720P,
CamcorderProfile.QUALITY_1080P,
CamcorderProfile.QUALITY_HIGH // existence guaranteed
};
VideoRecorder(Camera camera) {
mCamera = camera;
}
/**
* Initialize and start recording
*/
public void init() {
float fovW = mCamera.getParameters().getHorizontalViewAngle();
float fovH = mCamera.getParameters().getVerticalViewAngle();
mRecorder = new MediaRecorder();
mCamera.unlock();
mRecorder.setCamera(mCamera);
mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
CamcorderProfile profile = null;
for (int i: mPreferredProfiles) {
if (CamcorderProfile.hasProfile(i)) {
profile = CamcorderProfile.get(i);
mRecorder.setProfile(profile);
break;
}
}
writeVideoMetaInfo(profile.videoFrameWidth, profile.videoFrameHeight,
profile.videoFrameRate, fovW, fovH);
try {
mRecorder.setOutputFile(getVideoRecFilePath());
mRecorder.prepare();
} catch (IOException e) {
Log.e(TAG, "Preparation for recording failed.");
}
try {
mRecorder.start();
} catch (RuntimeException e) {
Log.e(TAG, "Starting recording failed.");
mRecorder.reset();
mRecorder.release();
mCamera.lock();
}
mRunning = true;
}
/**
* Stop recording
*/
public void end() {
if (mRunning) {
try {
mRecorder.stop();
mRecorder.reset();
mRecorder.release();
mCamera.lock();
} catch (RuntimeException e) {
e.printStackTrace();
Log.e(TAG, "Runtime error in stopping recording.");
}
}
mRecorder = null;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Log all raw sensor readings, for Rotation Vector sensor algorithms research
*/
class RawSensorLogger implements SensorEventListener {
private final String TAG = "RawSensorLogger";
private final static int SENSOR_RATE = SensorManager.SENSOR_DELAY_FASTEST;
private File mRecPath;
SensorManager mSensorManager;
Sensor mAccSensor, mGyroSensor, mMagSensor;
OutputStreamWriter mAccLogWriter, mGyroLogWriter, mMagLogWriter;
private float[] mRTemp = new float[16];
RawSensorLogger(File recPath) {
mRecPath = recPath;
}
/**
* Initialize and start recording
*/
public void init() {
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGyroSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE_UNCALIBRATED);
mMagSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED);
mSensorManager.registerListener(this, mAccSensor, SENSOR_RATE);
mSensorManager.registerListener(this, mGyroSensor, SENSOR_RATE);
mSensorManager.registerListener(this, mMagSensor, SENSOR_RATE);
try {
mAccLogWriter= new OutputStreamWriter(
new FileOutputStream(new File(mRecPath, "raw_acc.log")));
mGyroLogWriter= new OutputStreamWriter(
new FileOutputStream(new File(mRecPath, "raw_uncal_gyro.log")));
mMagLogWriter= new OutputStreamWriter(
new FileOutputStream(new File(mRecPath, "raw_uncal_mag.log")));
} catch (FileNotFoundException e) {
Log.e(TAG, "Sensor log file open failed: " + e.toString());
}
}
/**
* Stop recording and clean up
*/
public void end() {
mSensorManager.flush(this);
mSensorManager.unregisterListener(this);
try {
if (mAccLogWriter != null) {
OutputStreamWriter writer = mAccLogWriter;
mAccLogWriter = null;
writer.close();
}
if (mGyroLogWriter != null) {
OutputStreamWriter writer = mGyroLogWriter;
mGyroLogWriter = null;
writer.close();
}
if (mMagLogWriter != null) {
OutputStreamWriter writer = mMagLogWriter;
mMagLogWriter = null;
writer.close();
}
} catch (IOException e) {
Log.e(TAG, "Sensor log file close failed: " + e.toString());
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// do not care
}
@Override
public void onSensorChanged(SensorEvent event) {
OutputStreamWriter writer=null;
switch(event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
writer = mAccLogWriter;
break;
case Sensor.TYPE_GYROSCOPE_UNCALIBRATED:
writer = mGyroLogWriter;
break;
case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED:
writer = mMagLogWriter;
break;
}
if (writer!=null) {
float[] data = event.values;
try {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
writer.write(String.format("%d %f %f %f\r\n",
event.timestamp, data[0], data[1], data[2]));
}else // TYPE_GYROSCOPE_UNCALIBRATED and TYPE_MAGNETIC_FIELD_UNCALIBRATED
{
writer.write(String.format("%d %f %f %f %f %f %f\r\n", event.timestamp,
data[0], data[1], data[2], data[3], data[4], data[5]));
}
}catch (IOException e)
{
Log.e(TAG, "Write to raw sensor log file failed.");
}
}
}
}
/**
* Rotation sensor logger class
*/
class RVSensorLogger implements SensorEventListener {
private final String TAG = "RVSensorLogger";
private final static int SENSOR_RATE = 100;
RangeCoveredRegister mRegister;
int mAxis;
RVCVRecordActivity mActivity;
SensorManager mSensorManager;
Sensor mRVSensor;
OutputStreamWriter mLogWriter;
private float[] mRTemp = new float[16];
RVSensorLogger(RVCVRecordActivity activity) {
mActivity = activity;
}
/**
* Initialize and start recording
*/
public void init() {
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mRVSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
mSensorManager.registerListener(this, mRVSensor, SENSOR_RATE);
try {
mLogWriter= new OutputStreamWriter(
new FileOutputStream(mActivity.getSensorLogFilePath()));
} catch (FileNotFoundException e) {
Log.e(TAG, "Sensor log file open failed: " + e.toString());
}
}
/**
* Stop recording and clean up
*/
public void end() {
mSensorManager.flush(this);
mSensorManager.unregisterListener(this);
try {
if (mLogWriter != null) {
OutputStreamWriter writer = mLogWriter;
mLogWriter = null;
writer.close();
}
} catch (IOException e) {
Log.e(TAG, "Sensor log file close failed: " + e.toString());
}
updateRegister(null, AXIS_NONE);
}
private void onNewData(float[] data, long timestamp) {
// LOG
try {
if (mLogWriter != null) {
mLogWriter.write(String.format("%d %f %f %f %f\r\n", timestamp,
data[3], data[0], data[1], data[2]));
}
} catch (IOException e) {
Log.e(TAG, "Sensor log file write failed: " + e.toString());
}
// Update UI
if (mRegister != null) {
int d = 0;
int dx, dy, dz;
boolean valid = false;
SensorManager.getRotationMatrixFromVector(mRTemp, data);
dx = (int)(Math.asin(mRTemp[8])*(180.0/Math.PI));
dy = (int)(Math.asin(mRTemp[9])*(180.0/Math.PI));
dz = (int)((Math.atan2(mRTemp[4], mRTemp[0])+Math.PI)*(180.0/Math.PI));
switch(mAxis) {
case SensorManager.AXIS_X:
d = dx;
valid = (Math.abs(dy) < 30);
break;
case SensorManager.AXIS_Y:
d = dy;
valid = (Math.abs(dx) < 30);
break;
case SensorManager.AXIS_Z:
d = dz;
valid = (Math.abs(dx) < 20 && Math.abs(dy) < 20);
break;
}
if (valid) {
mRegister.update(d);
mActivity.redrawIndicator();
}
}
}
public void updateRegister(RangeCoveredRegister reg, int axis) {
mRegister = reg;
mAxis = axis;
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {
// do not care
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
onNewData(event.values, event.timestamp);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Controls the over all logic of record procedure: first x-direction, then y-direction and
* then z-direction.
*/
class RecordProcedureController implements Runnable {
private static final boolean LOCAL_LOGV = false;
private final RVCVRecordActivity mActivity;
private Thread mThread = null;
RecordProcedureController(RVCVRecordActivity activity) {
mActivity = activity;
mThread = new Thread(this);
mThread.start();
}
/**
* Run the record procedure
*/
public void run() {
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread Started.");
//start recording & logging
delay(2000);
init();
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread init() finished.");
// test 3 axis
// It is in YXZ order because UI element design use opposite definition
// of XY axis. To ensure the user see X Y Z, it is flipped here.
recordAxis(SensorManager.AXIS_Y);
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread axis 0 finished.");
recordAxis(SensorManager.AXIS_X);
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread axis 1 finished.");
recordAxis(SensorManager.AXIS_Z);
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread axis 2 finished.");
delay(1000);
end();
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread End.");
}
private void delay(int milli) {
try{
Thread.sleep(milli);
} catch(InterruptedException e) {
if (LOCAL_LOGV) Log.v(TAG, "Controller Thread Interrupted.");
}
}
private void init() {
// start video recording
mActivity.startRecordVideo();
// start sensor logging & listening
mActivity.startRecordSensor();
}
private void end() {
// stop video recording
mActivity.stopRecordVideo();
// stop sensor logging
mActivity.stopRecordSensor();
// notify ui complete
runOnUiThread(new Runnable(){
public void run() {
mActivity.notifyComplete();
}
});
}
private void recordAxis(int axis) {
// delay 2 seconds?
delay(1000);
// change ui
mActivity.switchAxisAsync(axis);
// play start sound
mActivity.playNotifySound(0);
// wait until axis covered
mActivity.waitUntilCovered(axis);
// play stop sound
mActivity.playNotifySound(1);
}
/**
* Force quit
*/
public void quit() {
mThread.interrupt();
try {
if (LOCAL_LOGV) Log.v(TAG, "Wait for controller to end");
// stop video recording
mActivity.stopRecordVideo();
// stop sensor logging
mActivity.stopRecordSensor();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}