diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:36 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:36 -0800 |
commit | 45fd319136b87911aae50269618b51c4a5902ad9 (patch) | |
tree | c653fbb77e1e9311fcdbd7fca8857e1c6bc3742e /src/com | |
parent | c308fa194a7b899cc7706429c3ea44b09966a2b1 (diff) | |
download | SoundRecorder-45fd319136b87911aae50269618b51c4a5902ad9.tar.gz |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/soundrecorder/Recorder.java | 243 | ||||
-rw-r--r-- | src/com/android/soundrecorder/SoundRecorder.java | 845 | ||||
-rw-r--r-- | src/com/android/soundrecorder/VUMeter.java | 90 |
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); + } +} |