summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commite2118f54af4c5215bd988979769e383292b9c9cb (patch)
tree5d9a6fd510c579ea0506bd68bdbaf0d76989137b /src
downloadandroid_packages_apps_SoundRecorder-e2118f54af4c5215bd988979769e383292b9c9cb.tar.gz
android_packages_apps_SoundRecorder-e2118f54af4c5215bd988979769e383292b9c9cb.tar.bz2
android_packages_apps_SoundRecorder-e2118f54af4c5215bd988979769e383292b9c9cb.zip
Initial Contribution
Diffstat (limited to 'src')
-rw-r--r--src/com/android/soundrecorder/Recorder.java235
-rw-r--r--src/com/android/soundrecorder/SoundRecorder.java615
-rw-r--r--src/com/android/soundrecorder/VUMeter.java90
3 files changed, 940 insertions, 0 deletions
diff --git a/src/com/android/soundrecorder/Recorder.java b/src/com/android/soundrecorder/Recorder.java
new file mode 100644
index 0000000..32fba51
--- /dev/null
+++ b/src/com/android/soundrecorder/Recorder.java
@@ -0,0 +1,235 @@
+package com.android.soundrecorder;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.media.MediaPlayer;
+import android.media.MediaRecorder;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+
+public class Recorder implements OnCompletionListener, OnErrorListener {
+ static final String SAMPLE_PREFIX = "recording";
+ static final String SAMPLE_EXTENSION = ".amr"; // this is a lie. See comment in com.google.android.mms.pdu.PduPersister
+ static final String SAMPLE_PATH_KEY = "sample_path";
+ static final String SAMPLE_LENGTH_KEY = "sample_length";
+
+ public static final int IDLE_STATE = 0;
+ public static final int RECORDING_STATE = 1;
+ public static final int PLAYING_STATE = 2;
+
+ int mState = IDLE_STATE;
+
+ public static final int NO_ERROR = 0;
+ public static final int SDCARD_ACCESS_ERROR = 1;
+ public static final int INTERNAL_ERROR = 2;
+
+ public interface OnStateChangedListener {
+ public void onStateChanged(int state);
+ public void onError(int error);
+ }
+ OnStateChangedListener mOnStateChangedListener = null;
+
+ long mSampleStart = 0; // time at which latest record or play operation started
+ int mSampleLength = 0; // length of current sample
+ File mSampleFile = null;
+
+ MediaRecorder mRecorder = null;
+ MediaPlayer mPlayer = null;
+
+ public Recorder() {
+ }
+
+ public void saveState(Bundle recorderState) {
+ recorderState.putString(SAMPLE_PATH_KEY, mSampleFile.getAbsolutePath());
+ recorderState.putInt(SAMPLE_LENGTH_KEY, mSampleLength);
+ }
+
+ public int getMaxAmplitude() {
+ if (mState != RECORDING_STATE)
+ return 0;
+ return mRecorder.getMaxAmplitude();
+ }
+
+ public void restoreState(Bundle recorderState) {
+ String samplePath = recorderState.getString(SAMPLE_PATH_KEY);
+ if (samplePath == null)
+ return;
+ int sampleLength = recorderState.getInt(SAMPLE_LENGTH_KEY, -1);
+ if (sampleLength == -1)
+ return;
+
+ File file = new File(samplePath);
+ if (!file.exists())
+ return;
+ if (mSampleFile != null
+ && mSampleFile.getAbsolutePath().compareTo(file.getAbsolutePath()) == 0)
+ return;
+
+ delete();
+ mSampleFile = file;
+ mSampleLength = sampleLength;
+
+ signalStateChanged(IDLE_STATE);
+ }
+
+ public void setOnStateChangedListener(OnStateChangedListener listener) {
+ mOnStateChangedListener = listener;
+ }
+
+ public int state() {
+ return mState;
+ }
+
+ public int progress() {
+ if (mState == RECORDING_STATE || mState == PLAYING_STATE)
+ return (int) ((System.currentTimeMillis() - mSampleStart)/1000);
+ return 0;
+ }
+
+ public int sampleLength() {
+ return mSampleLength;
+ }
+
+ public File sampleFile() {
+ return mSampleFile;
+ }
+
+ /**
+ * Resets the recorder state. If a sample was recorded, the file is deleted.
+ */
+ public void delete() {
+ stop();
+
+ if (mSampleFile != null)
+ mSampleFile.delete();
+
+ mSampleFile = null;
+ mSampleLength = 0;
+
+ signalStateChanged(IDLE_STATE);
+ }
+
+ /**
+ * Resets the recorder state. If a sample was recorded, the file is left on disk and will
+ * be reused for a new recording.
+ */
+ public void clear() {
+ stop();
+
+ mSampleLength = 0;
+
+ signalStateChanged(IDLE_STATE);
+ }
+
+ public void startRecording() {
+ stop();
+
+ if (mSampleFile == null) {
+ File sampleDir = Environment.getExternalStorageDirectory();
+ if (!sampleDir.canWrite()) // Workaround for broken sdcard support on the device.
+ sampleDir = new File("/sdcard/sdcard");
+
+ try {
+ mSampleFile = File.createTempFile(SAMPLE_PREFIX, SAMPLE_EXTENSION,
+ sampleDir);
+ } catch (IOException e) {
+ setError(SDCARD_ACCESS_ERROR);
+ return;
+ }
+ }
+
+ mRecorder = new MediaRecorder();
+ mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+ mRecorder.setOutputFile(mSampleFile.getAbsolutePath());
+ mRecorder.prepare();
+ mRecorder.start();
+
+ mSampleStart = System.currentTimeMillis();
+ setState(RECORDING_STATE);
+ }
+
+ public void stopRecording() {
+ if (mRecorder == null)
+ return;
+
+ mRecorder.stop();
+ mRecorder.release();
+ mRecorder = null;
+
+ mSampleLength = (int)( (System.currentTimeMillis() - mSampleStart)/1000 );
+ setState(IDLE_STATE);
+ }
+
+ public void startPlayback() {
+ stop();
+
+ mPlayer = new MediaPlayer();
+ try {
+ mPlayer.setDataSource(mSampleFile.getAbsolutePath());
+ mPlayer.setOnCompletionListener(this);
+ mPlayer.setOnErrorListener(this);
+ mPlayer.prepare();
+ mPlayer.start();
+ } catch (IllegalArgumentException e) {
+ setError(INTERNAL_ERROR);
+ mPlayer = null;
+ return;
+ } catch (IOException e) {
+ setError(SDCARD_ACCESS_ERROR);
+ mPlayer = null;
+ return;
+ }
+
+ mSampleStart = System.currentTimeMillis();
+ setState(PLAYING_STATE);
+ }
+
+ public void stopPlayback() {
+ if (mPlayer == null) // we were not in playback
+ return;
+
+ mPlayer.stop();
+ mPlayer.release();
+ mPlayer = null;
+ setState(IDLE_STATE);
+ }
+
+ public void stop() {
+ stopRecording();
+ stopPlayback();
+ }
+
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ stop();
+ setError(SDCARD_ACCESS_ERROR);
+ return true;
+ }
+
+ public void onCompletion(MediaPlayer mp) {
+ stop();
+ }
+
+ private void setState(int state) {
+ if (state == mState)
+ return;
+
+ mState = state;
+ signalStateChanged(mState);
+ }
+
+ private void signalStateChanged(int state) {
+ if (mOnStateChangedListener != null)
+ mOnStateChangedListener.onStateChanged(state);
+ }
+
+ private void setError(int error) {
+ if (mOnStateChangedListener != null)
+ mOnStateChangedListener.onError(error);
+ }
+}
diff --git a/src/com/android/soundrecorder/SoundRecorder.java b/src/com/android/soundrecorder/SoundRecorder.java
new file mode 100644
index 0000000..a06815f
--- /dev/null
+++ b/src/com/android/soundrecorder/SoundRecorder.java
@@ -0,0 +1,615 @@
+package com.android.soundrecorder;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.StatFs;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/*
+ * The file grows in jumps every five seconds or so. This class interpolates in between the jumps
+ * so we get a smooth countdown.
+ */
+class DiskSpaceCalculator {
+ private File mFile = null;
+ private long mBytesPerSecond = -1;
+ private long mBytesRemaining;
+
+ private long mStartTime;
+ private long mStartBytesRemaining;
+
+ private long mPollTime;
+
+ public DiskSpaceCalculator() {
+ mFile = Environment.getExternalStorageDirectory();
+ }
+
+ public void reset() {
+ mBytesPerSecond = -1;
+ mBytesRemaining = -1;
+ mStartBytesRemaining = -1;
+ }
+
+ /*
+ * Updates mBytesPerSecond, returns ammount of disk space available
+ */
+ public long pollDiskSpace() {
+ StatFs fs = new StatFs(mFile.getAbsolutePath());
+ long numBlocks = fs.getAvailableBlocks();
+ long blockSize = fs.getBlockSize();
+ long b = numBlocks * blockSize;
+ long t = System.currentTimeMillis();
+
+ if (b == mBytesRemaining)
+ return b; // nothing changed, don't recalculate mBytePerSecond
+
+ mPollTime = t;
+ mBytesRemaining = b;
+
+ if (mBytesRemaining > mStartBytesRemaining) {
+ // first call or space got freed up, reset the calculation
+ mStartBytesRemaining = mBytesRemaining;
+ mStartTime = mPollTime;
+ return b;
+ }
+
+ long size = mStartBytesRemaining - mBytesRemaining;
+ long time = (mPollTime - mStartTime)/1000;
+ if (time > 0)
+ mBytesPerSecond = size/time;
+
+ return b;
+ }
+
+ public long timeRemaining() {
+ if (mBytesPerSecond <= 0)
+ return -1;
+
+ long bytes = mBytesRemaining - mBytesPerSecond*(System.currentTimeMillis() - mPollTime)/1000;
+ return bytes/mBytesPerSecond;
+ }
+
+}
+
+public class SoundRecorder extends Activity
+ implements Button.OnClickListener, Recorder.OnStateChangedListener {
+ static final String TAG = "SoundRecorder";
+ static final String STATE_FILE_NAME = "soundrecorder.state";
+ static final String MIME_TYPE = "audio/amr"; // this is a lie. See comment in com.google.android.mms.pdu.PduPersister
+ static final String RECORDER_STATE_KEY = "recorder_state";
+ static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted";
+
+ Recorder mRecorder;
+ boolean mSampleInterrupted = false;
+ String mErrorUiMessage = null; // Some error messages are displayed in the UI,
+ // not a dialog. This happens when a recording
+ // is interrupted for some reason.
+
+ DiskSpaceCalculator mDiskSpaceCalculator;
+
+ String mTimerFormat;
+ final Handler mHandler = new Handler();
+ Runnable mUpdateTimer = new Runnable() {
+ public void run() { updateTimerView(); }
+ };
+
+ ImageButton mRecordButton;
+ ImageButton mPlayButton;
+ ImageButton mStopButton;
+
+ ImageView mStateLED;
+ TextView mStateMessage1;
+ TextView mStateMessage2;
+ ProgressBar mStateProgressBar;
+ TextView mTimerView;
+
+ LinearLayout mExitButtons;
+ Button mAcceptButton;
+ Button mDiscardButton;
+ VUMeter mVUMeter;
+
+ @Override
+ public void onCreate(Bundle icycle) {
+ super.onCreate(icycle);
+
+ setContentView(R.layout.main);
+
+ mRecorder = new Recorder();
+ mRecorder.setOnStateChangedListener(this);
+ mDiskSpaceCalculator = new DiskSpaceCalculator();
+
+ initResourceRefs();
+
+ setResult(RESULT_CANCELED);
+
+ if (icycle != null) {
+ Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY);
+ if (recorderState != null) {
+ mRecorder.restoreState(recorderState);
+ mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false);
+ }
+ }
+
+ updateUi();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ setContentView(R.layout.main);
+ initResourceRefs();
+ updateUi();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (mRecorder.sampleLength() == 0)
+ return;
+
+ Bundle recorderState = new Bundle();
+
+ mRecorder.saveState(recorderState);
+ recorderState.putBoolean(SAMPLE_INTERRUPTED_KEY, mSampleInterrupted);
+
+ outState.putBundle(RECORDER_STATE_KEY, recorderState);
+ }
+
+ private void initResourceRefs() {
+ mRecordButton = (ImageButton) findViewById(R.id.recordButton);
+ mPlayButton = (ImageButton) findViewById(R.id.playButton);
+ mStopButton = (ImageButton) findViewById(R.id.stopButton);
+
+ mStateLED = (ImageView) findViewById(R.id.stateLED);
+ mStateMessage1 = (TextView) findViewById(R.id.stateMessage1);
+ mStateMessage2 = (TextView) findViewById(R.id.stateMessage2);
+ mStateProgressBar = (ProgressBar) findViewById(R.id.stateProgressBar);
+ mTimerView = (TextView) findViewById(R.id.timerView);
+
+ mExitButtons = (LinearLayout) findViewById(R.id.exitButtons);
+ mAcceptButton = (Button) findViewById(R.id.acceptButton);
+ mDiscardButton = (Button) findViewById(R.id.discardButton);
+ mVUMeter = (VUMeter) findViewById(R.id.uvMeter);
+
+ mRecordButton.setOnClickListener(this);
+ mPlayButton.setOnClickListener(this);
+ mStopButton.setOnClickListener(this);
+ mAcceptButton.setOnClickListener(this);
+ mDiscardButton.setOnClickListener(this);
+
+ mTimerFormat = getResources().getString(R.string.timer_format);
+
+ mVUMeter.setRecorder(mRecorder);
+ }
+
+ public void onClick(View button) {
+ if (!button.isEnabled())
+ return;
+
+ switch (button.getId()) {
+ case R.id.recordButton:
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.insert_sd_card);
+ updateUi();
+ } else if (mDiskSpaceCalculator.pollDiskSpace() < 1024) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.storage_is_full);
+ updateUi();
+ } else {
+ mRecorder.startRecording();
+ }
+ break;
+ case R.id.playButton:
+ mRecorder.startPlayback();
+ break;
+ case R.id.stopButton:
+ mRecorder.stop();
+ break;
+ case R.id.acceptButton:
+ mRecorder.stop();
+ saveSample();
+ finish();
+ break;
+ case R.id.discardButton:
+ mRecorder.delete();
+ finish();
+ break;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ switch (mRecorder.state()) {
+ case Recorder.IDLE_STATE:
+ if (mRecorder.sampleLength() > 0)
+ saveSample();
+ finish();
+ break;
+ case Recorder.PLAYING_STATE:
+ mRecorder.stop();
+ saveSample();
+ break;
+ case Recorder.RECORDING_STATE:
+ mRecorder.clear();
+ break;
+ }
+ return true;
+ } else {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ mRecorder.stop();
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ mSampleInterrupted = mRecorder.state() == Recorder.RECORDING_STATE;
+ mRecorder.stop();
+
+ super.onPause();
+ }
+
+ private void saveSample() {
+ if (mRecorder.sampleLength() == 0)
+ return;
+ Uri uri = this.addToMediaDB(mRecorder.sampleFile());
+ if (uri == null)
+ return;
+ setResult(RESULT_OK, new Intent().setData(uri));
+ }
+
+ /*
+ * A simple utility to do a query into the databases.
+ */
+ private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ try {
+ ContentResolver resolver = getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ /*
+ * Add the given audioId to the playlist with the given playlistId; and maintain the
+ * play_order in the playlist.
+ */
+ private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) {
+ String[] cols = new String[] {
+ "count(*)"
+ };
+ Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
+ Cursor cur = resolver.query(uri, cols, null, null, null);
+ cur.moveToFirst();
+ final int base = cur.getInt(0);
+ cur.close();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId));
+ values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
+ resolver.insert(uri, values);
+ }
+
+ /*
+ * Obtain the id for the default play list from the audio_playlists table.
+ */
+ private int getPlaylistId(Resources res) {
+ Uri uri = MediaStore.Audio.Playlists.getContentUri("external");
+ final String[] ids = new String[] { MediaStore.Audio.Playlists._ID };
+ final String where = MediaStore.Audio.Playlists.NAME + "=?";
+ final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) };
+ Cursor cursor = query(uri, ids, where, args, null);
+ if (cursor == null) {
+ Log.v(TAG, "query returns null");
+ }
+ int id = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
+ }
+ cursor.close();
+ return id;
+ }
+
+ /*
+ * Create a playlist with the given default playlist name, if no such playlist exists.
+ */
+ private Uri createPlaylist(Resources res, ContentResolver resolver) {
+ ContentValues cv = new ContentValues();
+ cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name));
+ Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
+ if (uri == null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(R.string.error_mediadb_new_record)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ }
+ return uri;
+ }
+
+ /*
+ * Adds file and returns content uri.
+ */
+ private Uri addToMediaDB(File file) {
+ Resources res = getResources();
+ ContentValues cv = new ContentValues();
+ long current = System.currentTimeMillis();
+ long modDate = file.lastModified();
+ Date date = new Date(current);
+ SimpleDateFormat formatter = new SimpleDateFormat(
+ res.getString(R.string.audio_db_title_format));
+ String title = formatter.format(date);
+
+ // Lets label the recorded audio file as NON-MUSIC so that the file
+ // won't be displayed automatically, except for in the playlist.
+ cv.put(MediaStore.Audio.Media.IS_MUSIC, "0");
+
+ cv.put(MediaStore.Audio.Media.TITLE, title);
+ cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
+ cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000));
+ cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000));
+// cv.put(MediaStore.Audio.MediaColumns.DURATION, mRecordingLength);
+ cv.put(MediaStore.Audio.Media.MIME_TYPE, MIME_TYPE);
+ cv.put(MediaStore.Audio.Media.ARTIST,
+ res.getString(R.string.audio_db_artist_name));
+ cv.put(MediaStore.Audio.Media.ALBUM,
+ res.getString(R.string.audio_db_album_name));
+ Log.d(TAG, "Inserting audio record: " + cv.toString());
+ ContentResolver resolver = getContentResolver();
+ Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ Log.d(TAG, "ContentURI: " + base);
+ Uri result = resolver.insert(base, cv);
+ if (result == null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(R.string.error_mediadb_new_record)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ return null;
+ }
+ if (getPlaylistId(res) == -1) {
+ createPlaylist(res, resolver);
+ }
+ int audioId = Integer.valueOf(result.getLastPathSegment());
+ addToPlaylist(resolver, audioId, getPlaylistId(res));
+
+ // Notify those applications such as Music listening to the
+ // scanner events that a recorded audio file just created.
+ sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
+ return result;
+ }
+
+ /**
+ * This is the big MM:SS timer.
+ */
+ private void updateTimerView() {
+ Resources res = getResources();
+ int state = mRecorder.state();
+
+ boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE;
+
+ long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength();
+ String timeStr = String.format(mTimerFormat, time/60, time%60);
+ mTimerView.setText(timeStr);
+
+ if (state == Recorder.PLAYING_STATE) {
+ mStateProgressBar.setProgress((int)(100*time/mRecorder.sampleLength()));
+ } else if (state == Recorder.RECORDING_STATE) {
+ updateTimeRemaining();
+ }
+
+ if (ongoing)
+ mHandler.postDelayed(mUpdateTimer, 1000);
+ }
+
+ private void updateTimeRemaining() {
+ mDiskSpaceCalculator.pollDiskSpace();
+ long t = mDiskSpaceCalculator.timeRemaining();
+
+ if (t == -1) {
+ mStateMessage1.setText("");
+ return;
+ }
+
+ t -= 5; // safety buffer of 5 secs
+
+ if (t <= 0) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.storage_is_full);
+ mRecorder.stop();
+ return;
+ }
+
+ Resources res = getResources();
+ String timeStr = "";
+
+ if (t < 60)
+ timeStr = String.format(res.getString(R.string.sec_available), t);
+ else if (t < 540)
+ timeStr = String.format(res.getString(R.string.min_available), t/60 + 1);
+
+ mStateMessage1.setText(timeStr);
+ }
+
+ /**
+ * Shows/hides the appropriate child views for the new state.
+ */
+ private void updateUi() {
+ Resources res = getResources();
+
+ switch (mRecorder.state()) {
+ case Recorder.IDLE_STATE:
+ if (mRecorder.sampleLength() == 0) {
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(false);
+ mStopButton.setFocusable(false);
+ mRecordButton.requestFocus();
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.VISIBLE);
+ mStateLED.setImageResource(R.drawable.idle_led);
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.press_record));
+
+ mExitButtons.setVisibility(View.INVISIBLE);
+ mVUMeter.setVisibility(View.VISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.record_your_message));
+ } else {
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(true);
+ mPlayButton.setFocusable(true);
+ mStopButton.setEnabled(false);
+ mStopButton.setFocusable(false);
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.INVISIBLE);
+ mStateMessage2.setVisibility(View.INVISIBLE);
+
+ mExitButtons.setVisibility(View.VISIBLE);
+ mVUMeter.setVisibility(View.INVISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.message_recorded));
+ }
+
+ if (mSampleInterrupted) {
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.recording_stopped));
+ mStateLED.setImageResource(R.drawable.idle_led);
+ mStateLED.setVisibility(View.VISIBLE);
+ }
+
+ if (mErrorUiMessage != null) {
+ mStateMessage1.setText(mErrorUiMessage);
+ mStateMessage1.setVisibility(View.VISIBLE);
+ }
+
+ break;
+ case Recorder.RECORDING_STATE:
+ mRecordButton.setEnabled(false);
+ mRecordButton.setFocusable(false);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(true);
+ mStopButton.setFocusable(true);
+
+ mStateMessage1.setVisibility(View.VISIBLE);
+ mStateLED.setVisibility(View.VISIBLE);
+ mStateLED.setImageResource(R.drawable.recording_led);
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.recording));
+
+ mExitButtons.setVisibility(View.INVISIBLE);
+ mVUMeter.setVisibility(View.VISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.record_your_message));
+
+ break;
+
+ case Recorder.PLAYING_STATE:
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(true);
+ mStopButton.setFocusable(true);
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.INVISIBLE);
+ mStateMessage2.setVisibility(View.INVISIBLE);
+
+ mExitButtons.setVisibility(View.VISIBLE);
+ mVUMeter.setVisibility(View.INVISIBLE);
+
+ mStateProgressBar.setVisibility(View.VISIBLE);
+
+ setTitle(res.getString(R.string.review_message));
+
+ break;
+ }
+
+ updateTimerView();
+ mVUMeter.invalidate();
+ }
+
+ public void onStateChanged(int state) {
+ if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) {
+ mSampleInterrupted = false;
+ mErrorUiMessage = null;
+ }
+
+ if (state == Recorder.RECORDING_STATE)
+ mDiskSpaceCalculator.reset();
+
+ updateUi();
+ }
+
+ public void onError(int error) {
+ Resources res = getResources();
+
+ String message = null;
+ switch (error) {
+ case Recorder.SDCARD_ACCESS_ERROR:
+ message = res.getString(R.string.error_sdcard_access);
+ break;
+ case Recorder.INTERNAL_ERROR:
+ message = res.getString(R.string.error_app_internal);
+ break;
+ }
+ if (message != null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(message)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ }
+ }
+}
diff --git a/src/com/android/soundrecorder/VUMeter.java b/src/com/android/soundrecorder/VUMeter.java
new file mode 100644
index 0000000..6aee87d
--- /dev/null
+++ b/src/com/android/soundrecorder/VUMeter.java
@@ -0,0 +1,90 @@
+package com.android.soundrecorder;
+
+import java.util.Map;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class VUMeter extends View {
+ static final float PIVOT_RADIUS = 3.5f;
+ static final float PIVOT_Y_OFFSET = 10f;
+ static final float SHADOW_OFFSET = 2.0f;
+ static final float DROPOFF_STEP = 0.18f;
+ static final float SURGE_STEP = 0.35f;
+ static final long ANIMATION_INTERVAL = 70;
+
+ Paint mPaint, mShadow;
+ float mCurrentAngle;
+
+ Recorder mRecorder;
+
+ public VUMeter(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public VUMeter(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ void init(Context context) {
+ Drawable background = context.getResources().getDrawable(R.drawable.vumeter);
+ setBackgroundDrawable(background);
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setColor(Color.WHITE);
+ mShadow = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mShadow.setColor(Color.argb(60, 0, 0, 0));
+
+ mRecorder = null;
+
+ mCurrentAngle = 0;
+ }
+
+ public void setRecorder(Recorder recorder) {
+ mRecorder = recorder;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final float minAngle = (float)Math.PI/8;
+ final float maxAngle = (float)Math.PI*7/8;
+
+ float angle = minAngle;
+ if (mRecorder != null)
+ angle += (float)(maxAngle - minAngle)*mRecorder.getMaxAmplitude()/32768;
+
+ if (angle > mCurrentAngle)
+ mCurrentAngle = angle;
+ else
+ mCurrentAngle = Math.max(angle, mCurrentAngle - DROPOFF_STEP);
+
+ mCurrentAngle = Math.min(maxAngle, mCurrentAngle);
+
+ float w = getWidth();
+ float h = getHeight();
+ float pivotX = w/2;
+ float pivotY = h - PIVOT_RADIUS - PIVOT_Y_OFFSET;
+ float l = h*4/5;
+ float sin = (float) Math.sin(mCurrentAngle);
+ float cos = (float) Math.cos(mCurrentAngle);
+ float x0 = pivotX - l*cos;
+ float y0 = pivotY - l*sin;
+ canvas.drawLine(x0 + SHADOW_OFFSET, y0 + SHADOW_OFFSET, pivotX + SHADOW_OFFSET, pivotY + SHADOW_OFFSET, mShadow);
+ canvas.drawCircle(pivotX + SHADOW_OFFSET, pivotY + SHADOW_OFFSET, PIVOT_RADIUS, mShadow);
+ canvas.drawLine(x0, y0, pivotX, pivotY, mPaint);
+ canvas.drawCircle(pivotX, pivotY, PIVOT_RADIUS, mPaint);
+
+ if (mRecorder != null && mRecorder.state() == Recorder.RECORDING_STATE)
+ postInvalidateDelayed(ANIMATION_INTERVAL);
+ }
+}