package com.pimentoso.android.laptimer; import java.io.IOException; import java.util.ArrayList; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.ImageFormat; import android.hardware.Camera; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; public class TimerActivity extends Activity implements SurfaceHolder.Callback, Camera.PreviewCallback, OnClickListener { // elementi del layout SurfaceView mSurfaceView; SurfaceHolder mSurfaceHolder; Camera mCamera; TextView timerLabel; TextView statusLabel; TextView lap1Label; TextView lap2Label; TextView lap3Label; TextView lapBestLabel; Button startButton; // flags boolean isPreviewRunning = false; boolean isCalibrating = false; boolean isCalibrated = false; boolean isStarted = false; boolean isTimerRunning = false; boolean caughtPreviousFrame = false; // contatore dei frame per calibrazione int frame = 0; // offset dei pixel da controllare int[] pixelOffset = new int[3]; // array di calibrazione int[][] calibrateRange = new int[3][20]; // valori finali di calibrazione int[] calibrateValue = new int[3]; // soglia di differenza di luminosità per catchare il frame public static int calibrateThreshold = 10; // millisecondi di ultimo catch long mLastCatchTime = 0; // tempo del giro migliore long bestLap = 0; // tempi dei giri // long[] laps = new long[3]; ArrayList<Long> laps = new ArrayList<Long>(); // contatore dei giri int lapCount = 0; Handler mHandler = new Handler(); FPSCounter fps; long mStartTime = 0L; StringBuilder lapBuffer; StringBuilder timeBuffer; static byte[] frameBuffer1; static byte[] frameBuffer2; static byte[] frameBuffer3; private Runnable mUpdateTimeTask = new Runnable() { public void run() { long millis = SystemClock.uptimeMillis() - mStartTime; timerLabel.setText(convertTime(millis)); mHandler.postAtTime(this, SystemClock.uptimeMillis() + 40); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); findViewById(R.id.button_start).setOnClickListener(this); findViewById(R.id.button_calibrate).setOnClickListener(this); mSurfaceView = (SurfaceView) findViewById(R.id.surface_camera); mSurfaceHolder = mSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); String threshold = getPreferences(MODE_PRIVATE).getString("sensitivity", "15"); calibrateThreshold = Integer.valueOf(threshold); timerLabel = (TextView) findViewById(R.id.text_timer); statusLabel = (TextView) findViewById(R.id.text_status); lap1Label = (TextView) findViewById(R.id.text_lap_1); lap2Label = (TextView) findViewById(R.id.text_lap_2); lap3Label = (TextView) findViewById(R.id.text_lap_3); lapBestLabel = (TextView) findViewById(R.id.text_lap_best); startButton = (Button) findViewById(R.id.button_start); statusLabel.setText(getString(R.string.label_status_init)); startButton.setEnabled(false); fps = new FPSCounter(); } @Override public void onStart() { super.onStart(); // show help if (getPreferences(MODE_PRIVATE).getString("first_time", "1").equals("1")) { showAlertBox(); getPreferences(MODE_PRIVATE).edit().putString("first_time", "0").commit(); } } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { } @Override public void surfaceCreated(SurfaceHolder arg0) { synchronized (this) { try { mCamera = Camera.open(); } catch (RuntimeException e) { // camera service already in use: schianta new AlertDialog.Builder(this).setMessage(getString(R.string.error_camera_locked_text)).setTitle("Error").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { TimerActivity.this.finish(); } }).show(); return; } if (mCamera == null) { // camera not found: schianta new AlertDialog.Builder(this).setMessage(getString(R.string.error_camera_null_text)).setTitle("Error").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { TimerActivity.this.finish(); } }).show(); return; } Camera.Parameters parameters = mCamera.getParameters(); Camera.Size mCameraSize = parameters.getPreviewSize(); int bytesPerPixel = ImageFormat.getBitsPerPixel(parameters.getPreviewFormat()); int bufferSize = (mCameraSize.width * mCameraSize.height * bytesPerPixel) >> 3; frameBuffer1 = new byte[bufferSize]; frameBuffer2 = new byte[bufferSize]; frameBuffer3 = new byte[bufferSize]; mCamera.addCallbackBuffer(frameBuffer1); mCamera.addCallbackBuffer(frameBuffer2); mCamera.addCallbackBuffer(frameBuffer3); pixelOffset[0] = (int) (mCameraSize.width / 2) + (mCameraSize.width * (int) (mCameraSize.height * 0.1)); pixelOffset[1] = (int) (mCameraSize.width / 2) + (mCameraSize.width * (int) (mCameraSize.height * 0.5)); pixelOffset[2] = (int) (mCameraSize.width / 2) + (mCameraSize.width * (int) (mCameraSize.height * 0.9)); mCamera.setDisplayOrientation(90); try { mCamera.setPreviewDisplay(arg0); } catch (IOException e) { Log.e("Camera", "Could not set preview display"); } mCamera.setPreviewCallbackWithBuffer(this); mCamera.startPreview(); isPreviewRunning = true; } } @Override public void surfaceDestroyed(SurfaceHolder arg0) { synchronized (this) { try { if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); isPreviewRunning = false; } } catch (Exception e) { Log.e("Camera", e.getMessage()); } finally { if (mCamera != null) { mCamera.release(); } } } } @Override public void onPreviewFrame(byte[] yuv, Camera arg1) { int value0 = (int) yuv[pixelOffset[0]] & 0xFF; int value1 = (int) yuv[pixelOffset[1]] & 0xFF; int value2 = (int) yuv[pixelOffset[2]] & 0xFF; // sto calibrando... if (isCalibrating) { frame++; calibrateRange[0][frame - 1] = value0; calibrateRange[1][frame - 1] = value1; calibrateRange[2][frame - 1] = value2; if (frame >= 20) { // finito di calibrare isCalibrating = false; isCalibrated = true; startButton.setEnabled(true); statusLabel.setText(getString(R.string.label_status_ready)); // calcolo la media int tot0 = 0, tot1 = 0, tot2 = 0; for (int i = 0; i < 20; i++) { tot0 += calibrateRange[0][i]; tot1 += calibrateRange[1][i]; tot2 += calibrateRange[2][i]; } calibrateValue[0] = tot0 / 20; calibrateValue[1] = tot1 / 20; calibrateValue[2] = tot2 / 20; } } // sono in ascolto di variazioni else if (isStarted) { // catch del frame if (isCalibrated && (value0 < calibrateValue[0] - calibrateThreshold || value0 > calibrateValue[0] + calibrateThreshold || value1 < calibrateValue[1] - calibrateThreshold || value1 > calibrateValue[1] + calibrateThreshold || value2 < calibrateValue[2] - calibrateThreshold || value2 > calibrateValue[2] + calibrateThreshold)) { // se ho catchato il frame precedente, ignoro if (!caughtPreviousFrame) { caughtPreviousFrame = true; if (isTimerRunning) { // calcolo dei lap long catchTime = SystemClock.uptimeMillis(); lapCount++; long lapTime = catchTime - mLastCatchTime; laps.add(lapTime); if (lapCount == 1) { bestLap = lapTime; } else if (bestLap > lapTime) { bestLap = lapTime; } printLaps(); mLastCatchTime = catchTime; } else { // devo far partire il timer isTimerRunning = true; mStartTime = SystemClock.uptimeMillis(); mLastCatchTime = mStartTime; mHandler.removeCallbacks(mUpdateTimeTask); mHandler.postDelayed(mUpdateTimeTask, 50); } } } else { caughtPreviousFrame = false; } } mCamera.addCallbackBuffer(yuv); fps.logFrame(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.button_start: { if (!isCalibrated || isCalibrating) { // non è calibrato break; } if (isStarted) { // ho stoppato startButton.setText("Start"); statusLabel.setText(getString(R.string.label_status_ready)); isStarted = false; isTimerRunning = false; mHandler.removeCallbacks(mUpdateTimeTask); } else { // ho startato startButton.setText("Stop"); statusLabel.setText(getString(R.string.label_status_started)); timerLabel.setText("0:00:0"); isStarted = true; mStartTime = 0L; lapCount = 0; // resetto i laps laps = new ArrayList<Long>(); bestLap = 0; printLaps(); } break; } case R.id.button_calibrate: { if (isTimerRunning) { // devo stoppare prima di calibrare break; } // ho pigiato calibra, devo resettare tutto statusLabel.setText(getString(R.string.label_status_calibrating)); timerLabel.setText("0:00:0"); frame = 0; mStartTime = 0L; lapCount = 0; isStarted = false; isTimerRunning = false; isCalibrating = true; isCalibrated = false; // resetto i laps laps = new ArrayList<Long>(); bestLap = 0; printLaps(); break; } } } private String convertTime(long millis) { if (millis == 0) { return "0:00:0"; } timeBuffer = new StringBuilder(); int split = ((int) (millis / 100)) % 10; int seconds = (int) (millis / 1000); int minutes = seconds / 60; seconds = seconds % 60; if (seconds < 10) { timeBuffer.append(minutes).append(":0").append(seconds).append(":").append(split); } else { timeBuffer.append(minutes).append(":").append(seconds).append(":").append(split); } return timeBuffer.toString(); } private void printLaps() { lapBuffer = new StringBuilder(); lapBuffer.append("Lap ").append(lapCount).append(": "); try { lapBuffer.append(convertTime(laps.get(laps.size() - 1))); } catch (IndexOutOfBoundsException e) { lapBuffer.append(convertTime(0)); } lap1Label.setText(lapBuffer.toString()); lapBuffer = new StringBuilder(); if (lapCount > 1) { lapBuffer.append("Lap ").append(lapCount - 1).append(": "); lap2Label.setVisibility(View.VISIBLE); } else { lapBuffer.append("Lap 0: "); } try { lapBuffer.append(convertTime(laps.get(laps.size() - 2))); } catch (IndexOutOfBoundsException e) { lapBuffer.append(convertTime(0)); } lap2Label.setText(lapBuffer.toString()); lapBuffer = new StringBuilder(); if (lapCount > 2) { lapBuffer.append("Lap ").append(lapCount - 2).append(": "); lap3Label.setVisibility(View.VISIBLE); } else { lapBuffer.append("Lap 0: "); } try { lapBuffer.append(convertTime(laps.get(laps.size() - 3))); } catch (IndexOutOfBoundsException e) { lapBuffer.append(convertTime(0)); } lap3Label.setText(lapBuffer.toString()); lapBuffer = new StringBuilder(); lapBuffer.append("Best lap: ").append(convertTime(bestLap)); lapBestLabel.setText(lapBuffer.toString()); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_tutorial: { showAlertBox(); return true; } case R.id.menu_sensitivity: { // devo stoppare tutto startButton.setText("Start"); statusLabel.setText(getString(R.string.label_status_ready)); isStarted = false; isTimerRunning = false; mHandler.removeCallbacks(mUpdateTimeTask); Intent i = new Intent(this, SensitivityDialogActivity.class); startActivity(i); return true; } case R.id.menu_email: { if (isStarted || isTimerRunning) { Toast.makeText(this, getString(R.string.error_timer_started), Toast.LENGTH_SHORT).show(); return true; } if (laps == null || laps.size() == 0) { Toast.makeText(this, getString(R.string.error_laps_empty), Toast.LENGTH_SHORT).show(); return true; } String emailBody = lapsToString(); final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); emailIntent.setType("plain/text"); emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, getString(R.string.app_name)); emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, emailBody); startActivity(Intent.createChooser(emailIntent, getString(R.string.menu_mail_laps))); return true; } } return false; } public void showAlertBox() { new AlertDialog.Builder(this).setMessage(getString(R.string.dialog_tutorial_text)).setTitle("Tutorial").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }).show(); } private String lapsToString() { StringBuilder s = new StringBuilder(); s.append("Mini 4WD Android Lap Timer data"); s.append("\n\n"); for (int i = 0; i < laps.size(); i++) { long lap = laps.get(i); s.append("Lap ").append(i + 1).append(": ").append(convertTime(lap)).append("\n"); } s.append("\n"); s.append("Best lap: " + convertTime(bestLap)); return s.toString(); } }