diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | e2118f54af4c5215bd988979769e383292b9c9cb (patch) | |
tree | 5d9a6fd510c579ea0506bd68bdbaf0d76989137b /src | |
download | android_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.java | 235 | ||||
-rw-r--r-- | src/com/android/soundrecorder/SoundRecorder.java | 615 | ||||
-rw-r--r-- | src/com/android/soundrecorder/VUMeter.java | 90 |
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); + } +} |