summaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:36 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:32:36 -0800
commit45fd319136b87911aae50269618b51c4a5902ad9 (patch)
treec653fbb77e1e9311fcdbd7fca8857e1c6bc3742e /src/com
parentc308fa194a7b899cc7706429c3ea44b09966a2b1 (diff)
downloadSoundRecorder-45fd319136b87911aae50269618b51c4a5902ad9.tar.gz
auto import from //depot/cupcake/@135843
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/soundrecorder/Recorder.java243
-rw-r--r--src/com/android/soundrecorder/SoundRecorder.java845
-rw-r--r--src/com/android/soundrecorder/VUMeter.java90
3 files changed, 1178 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..5fe41b0
--- /dev/null
+++ b/src/com/android/soundrecorder/Recorder.java
@@ -0,0 +1,243 @@
+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_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(int outputfileformat, String extension) {
+ 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, extension, sampleDir);
+ } catch (IOException e) {
+ setError(SDCARD_ACCESS_ERROR);
+ return;
+ }
+ }
+
+ mRecorder = new MediaRecorder();
+ mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mRecorder.setOutputFormat(outputfileformat);
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+ mRecorder.setOutputFile(mSampleFile.getAbsolutePath());
+
+ // Handle IOException
+ try {
+ mRecorder.prepare();
+ } catch(IOException exception) {
+ setError(INTERNAL_ERROR);
+ mRecorder.reset();
+ mRecorder.release();
+ mRecorder = null;
+ return;
+ }
+ 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..45eaa70
--- /dev/null
+++ b/src/com/android/soundrecorder/SoundRecorder.java
@@ -0,0 +1,845 @@
+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.Context;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.StatFs;
+import android.os.PowerManager.WakeLock;
+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;
+
+/**
+ * Calculates remaining recording time based on available disk space and
+ * optionally a maximum recording file size.
+ *
+ * The reason why this is not trivial is that the file grows in blocks
+ * every few seconds or so, while we want a smooth countdown.
+ */
+
+class RemainingTimeCalculator {
+ public static final int UNKNOWN_LIMIT = 0;
+ public static final int FILE_SIZE_LIMIT = 1;
+ public static final int DISK_SPACE_LIMIT = 2;
+
+ // which of the two limits we will hit (or have fit) first
+ private int mCurrentLowerLimit = UNKNOWN_LIMIT;
+
+ private File mSDCardDirectory;
+
+ // State for tracking file size of recording.
+ private File mRecordingFile;
+ private long mMaxBytes;
+
+ // Rate at which the file grows
+ private int mBytesPerSecond;
+
+ // time at which number of free blocks last changed
+ private long mBlocksChangedTime;
+ // number of available blocks at that time
+ private long mLastBlocks;
+
+ // time at which the size of the file has last changed
+ private long mFileSizeChangedTime;
+ // size of the file at that time
+ private long mLastFileSize;
+
+ public RemainingTimeCalculator() {
+ mSDCardDirectory = Environment.getExternalStorageDirectory();
+ }
+
+ /**
+ * If called, the calculator will return the minimum of two estimates:
+ * how long until we run out of disk space and how long until the file
+ * reaches the specified size.
+ *
+ * @param file the file to watch
+ * @param maxBytes the limit
+ */
+
+ public void setFileSizeLimit(File file, long maxBytes) {
+ mRecordingFile = file;
+ mMaxBytes = maxBytes;
+ }
+
+ /**
+ * Resets the interpolation.
+ */
+ public void reset() {
+ mCurrentLowerLimit = UNKNOWN_LIMIT;
+ mBlocksChangedTime = -1;
+ mFileSizeChangedTime = -1;
+ }
+
+ /**
+ * Returns how long (in seconds) we can continue recording.
+ */
+ public long timeRemaining() {
+ // Calculate how long we can record based on free disk space
+
+ StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
+ long blocks = fs.getAvailableBlocks();
+ long blockSize = fs.getBlockSize();
+ long now = System.currentTimeMillis();
+
+ if (mBlocksChangedTime == -1 || blocks != mLastBlocks) {
+ mBlocksChangedTime = now;
+ mLastBlocks = blocks;
+ }
+
+ /* The calculation below always leaves one free block, since free space
+ in the block we're currently writing to is not added. This
+ last block might get nibbled when we close and flush the file, but
+ we won't run out of disk. */
+
+ // at mBlocksChangedTime we had this much time
+ long result = mLastBlocks*blockSize/mBytesPerSecond;
+ // so now we have this much time
+ result -= (now - mBlocksChangedTime)/1000;
+
+ if (mRecordingFile == null) {
+ mCurrentLowerLimit = DISK_SPACE_LIMIT;
+ return result;
+ }
+
+ // If we have a recording file set, we calculate a second estimate
+ // based on how long it will take us to reach mMaxBytes.
+
+ mRecordingFile = new File(mRecordingFile.getAbsolutePath());
+ long fileSize = mRecordingFile.length();
+ if (mFileSizeChangedTime == -1 || fileSize != mLastFileSize) {
+ mFileSizeChangedTime = now;
+ mLastFileSize = fileSize;
+ }
+
+ long result2 = (mMaxBytes - fileSize)/mBytesPerSecond;
+ result2 -= (now - mFileSizeChangedTime)/1000;
+ result2 -= 1; // just for safety
+
+ mCurrentLowerLimit = result < result2
+ ? DISK_SPACE_LIMIT : FILE_SIZE_LIMIT;
+
+ return Math.min(result, result2);
+ }
+
+ /**
+ * Indicates which limit we will hit (or have hit) first, by returning one
+ * of FILE_SIZE_LIMIT or DISK_SPACE_LIMIT or UNKNOWN_LIMIT. We need this to
+ * display the correct message to the user when we hit one of the limits.
+ */
+ public int currentLowerLimit() {
+ return mCurrentLowerLimit;
+ }
+
+ /**
+ * Is there any point of trying to start recording?
+ */
+ public boolean diskSpaceAvailable() {
+ StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
+ // keep one free block
+ return fs.getAvailableBlocks() > 1;
+ }
+
+ /**
+ * Sets the bit rate used in the interpolation.
+ *
+ * @param bitRate the bit rate to set in bits/sec.
+ */
+ public void setBitRate(int bitRate) {
+ mBytesPerSecond = bitRate/8;
+ }
+}
+
+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 RECORDER_STATE_KEY = "recorder_state";
+ static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted";
+ static final String MAX_FILE_SIZE_KEY = "max_file_size";
+
+ static final String AUDIO_3GPP = "audio/3gpp";
+ static final String AUDIO_AMR = "audio/amr";
+ static final String AUDIO_ANY = "audio/*";
+
+ static final int BITRATE_AMR = 5900; // bits/sec
+ static final int BITRATE_3GPP = 5900;
+
+ WakeLock mWakeLock;
+ String mRequestedType = AUDIO_ANY;
+ 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.
+
+ long mMaxFileSize = -1; // can be specified in the intent
+ RemainingTimeCalculator mRemainingTimeCalculator;
+
+ 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;
+ private BroadcastReceiver mSDCardMountEventReceiver = null;
+
+ @Override
+ public void onCreate(Bundle icycle) {
+ super.onCreate(icycle);
+
+ Intent i = getIntent();
+ if (i != null) {
+ String s = i.getType();
+ if (AUDIO_AMR.equals(s) || AUDIO_3GPP.equals(s) || AUDIO_ANY.equals(s)) {
+ mRequestedType = s;
+ } else if (s != null) {
+ // we only support amr and 3gpp formats right now
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ final String EXTRA_MAX_BYTES
+ = android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES;
+ mMaxFileSize = i.getLongExtra(EXTRA_MAX_BYTES, -1);
+ }
+
+ if (AUDIO_ANY.equals(mRequestedType)) {
+ mRequestedType = AUDIO_3GPP;
+ }
+
+ setContentView(R.layout.main);
+
+ mRecorder = new Recorder();
+ mRecorder.setOnStateChangedListener(this);
+ mRemainingTimeCalculator = new RemainingTimeCalculator();
+
+ PowerManager pm
+ = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK,
+ "SoundRecorder");
+
+ initResourceRefs();
+
+ setResult(RESULT_CANCELED);
+ registerExternalStorageListener();
+ if (icycle != null) {
+ Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY);
+ if (recorderState != null) {
+ mRecorder.restoreState(recorderState);
+ mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false);
+ mMaxFileSize = recorderState.getLong(MAX_FILE_SIZE_KEY, -1);
+ }
+ }
+
+ 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);
+ recorderState.putLong(MAX_FILE_SIZE_KEY, mMaxFileSize);
+
+ outState.putBundle(RECORDER_STATE_KEY, recorderState);
+ }
+
+ /*
+ * Whenever the UI is re-created (due f.ex. to orientation change) we have
+ * to reinitialize references to the views.
+ */
+ 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);
+ }
+
+ /*
+ * Make sure we're not recording music playing in the background, ask
+ * the MediaPlaybackService to pause playback.
+ */
+ private void stopAudioPlayback() {
+ // Shamelessly copied from MediaPlaybackService.java, which
+ // should be public, but isn't.
+ Intent i = new Intent("com.android.music.musicservicecommand");
+ i.putExtra("command", "pause");
+
+ sendBroadcast(i);
+ }
+
+ /*
+ * Handle the buttons.
+ */
+ public void onClick(View button) {
+ if (!button.isEnabled())
+ return;
+
+ switch (button.getId()) {
+ case R.id.recordButton:
+ mRemainingTimeCalculator.reset();
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.insert_sd_card);
+ updateUi();
+ } else if (!mRemainingTimeCalculator.diskSpaceAvailable()) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.storage_is_full);
+ updateUi();
+ } else {
+ stopAudioPlayback();
+
+ if (AUDIO_AMR.equals(mRequestedType)) {
+ mRemainingTimeCalculator.setBitRate(BITRATE_AMR);
+ mRecorder.startRecording(MediaRecorder.OutputFormat.RAW_AMR, ".amr");
+ } else if (AUDIO_3GPP.equals(mRequestedType)) {
+ mRemainingTimeCalculator.setBitRate(BITRATE_3GPP);
+ mRecorder.startRecording(MediaRecorder.OutputFormat.THREE_GPP, ".3gpp");
+ } else {
+ throw new IllegalArgumentException("Invalid output file type requested");
+ }
+
+ if (mMaxFileSize != -1) {
+ mRemainingTimeCalculator.setFileSizeLimit(
+ mRecorder.sampleFile(), mMaxFileSize);
+ }
+ }
+ 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;
+ }
+ }
+
+ /*
+ * Handle the "back" hardware key.
+ */
+ @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();
+ }
+
+ /*
+ * If we have just recorded a smaple, this adds it to the media data base
+ * and sets the result to the sample's URI.
+ */
+ private void saveSample() {
+ if (mRecorder.sampleLength() == 0)
+ return;
+ Uri uri = null;
+ try {
+ uri = this.addToMediaDB(mRecorder.sampleFile());
+ } catch(UnsupportedOperationException ex) { // Database manipulation failure
+ return;
+ }
+ if (uri == null) {
+ return;
+ }
+ setResult(RESULT_OK, new Intent().setData(uri));
+ }
+
+ /*
+ * Called on destroy to unregister the SD card mount event receiver.
+ */
+ @Override
+ public void onDestroy() {
+ if (mSDCardMountEventReceiver != null) {
+ unregisterReceiver(mSDCardMountEventReceiver);
+ mSDCardMountEventReceiver = null;
+ }
+ super.onDestroy();
+ }
+
+ /*
+ * Registers an intent to listen for ACTION_MEDIA_EJECT/ACTION_MEDIA_MOUNTED
+ * notifications.
+ */
+ private void registerExternalStorageListener() {
+ if (mSDCardMountEventReceiver == null) {
+ mSDCardMountEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ mRecorder.delete();
+ } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ mSampleInterrupted = false;
+ updateUi();
+ }
+ }
+ };
+ IntentFilter iFilter = new IntentFilter();
+ iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ iFilter.addDataScheme("file");
+ registerReceiver(mSDCardMountEventReceiver, iFilter);
+ }
+ }
+
+ /*
+ * 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.Media.MIME_TYPE, mRequestedType);
+ 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;
+ }
+
+ /**
+ * Update the big MM:SS timer. If we are in playback, also update the
+ * progress bar.
+ */
+ 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);
+ }
+
+ /*
+ * Called when we're in recording state. Find out how much longer we can
+ * go on recording. If it's under 5 minutes, we display a count-down in
+ * the UI. If we've run out of time, stop the recording.
+ */
+ private void updateTimeRemaining() {
+ long t = mRemainingTimeCalculator.timeRemaining();
+
+ if (t <= 0) {
+ mSampleInterrupted = true;
+
+ int limit = mRemainingTimeCalculator.currentLowerLimit();
+ switch (limit) {
+ case RemainingTimeCalculator.DISK_SPACE_LIMIT:
+ mErrorUiMessage
+ = getResources().getString(R.string.storage_is_full);
+ break;
+ case RemainingTimeCalculator.FILE_SIZE_LIMIT:
+ mErrorUiMessage
+ = getResources().getString(R.string.max_length_reached);
+ break;
+ default:
+ mErrorUiMessage = null;
+ break;
+ }
+
+ 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();
+ }
+
+ /*
+ * Called when Recorder changed it's state.
+ */
+ public void onStateChanged(int state) {
+ if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) {
+ mSampleInterrupted = false;
+ mErrorUiMessage = null;
+ }
+
+ if (state == Recorder.RECORDING_STATE) {
+ mWakeLock.acquire(); // we don't want to go to sleep while recording
+ } else {
+ if (mWakeLock.isHeld())
+ mWakeLock.release();
+ }
+
+ updateUi();
+ }
+
+ /*
+ * Called when MediaPlayer encounters an error.
+ */
+ 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);
+ }
+}