diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:01 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:01 -0800 |
commit | cefd3e81da22167a56c89a8794c9b8dfe67a7673 (patch) | |
tree | fea078b53cf59b9688700747527265af91fdc53f /src/com | |
parent | e2118f54af4c5215bd988979769e383292b9c9cb (diff) | |
download | SoundRecorder-cefd3e81da22167a56c89a8794c9b8dfe67a7673.tar.gz |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/soundrecorder/Recorder.java | 20 | ||||
-rw-r--r-- | src/com/android/soundrecorder/SoundRecorder.java | 349 |
2 files changed, 296 insertions, 73 deletions
diff --git a/src/com/android/soundrecorder/Recorder.java b/src/com/android/soundrecorder/Recorder.java index 32fba51..5fe41b0 100644 --- a/src/com/android/soundrecorder/Recorder.java +++ b/src/com/android/soundrecorder/Recorder.java @@ -13,7 +13,6 @@ 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"; @@ -125,7 +124,7 @@ public class Recorder implements OnCompletionListener, OnErrorListener { signalStateChanged(IDLE_STATE); } - public void startRecording() { + public void startRecording(int outputfileformat, String extension) { stop(); if (mSampleFile == null) { @@ -134,8 +133,7 @@ public class Recorder implements OnCompletionListener, OnErrorListener { sampleDir = new File("/sdcard/sdcard"); try { - mSampleFile = File.createTempFile(SAMPLE_PREFIX, SAMPLE_EXTENSION, - sampleDir); + mSampleFile = File.createTempFile(SAMPLE_PREFIX, extension, sampleDir); } catch (IOException e) { setError(SDCARD_ACCESS_ERROR); return; @@ -144,10 +142,20 @@ public class Recorder implements OnCompletionListener, OnErrorListener { mRecorder = new MediaRecorder(); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mRecorder.setOutputFormat(outputfileformat); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mRecorder.setOutputFile(mSampleFile.getAbsolutePath()); - mRecorder.prepare(); + + // Handle IOException + try { + mRecorder.prepare(); + } catch(IOException exception) { + setError(INTERNAL_ERROR); + mRecorder.reset(); + mRecorder.release(); + mRecorder = null; + return; + } mRecorder.start(); mSampleStart = System.currentTimeMillis(); diff --git a/src/com/android/soundrecorder/SoundRecorder.java b/src/com/android/soundrecorder/SoundRecorder.java index a06815f..2338362 100644 --- a/src/com/android/soundrecorder/SoundRecorder.java +++ b/src/com/android/soundrecorder/SoundRecorder.java @@ -9,14 +9,20 @@ 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; @@ -28,86 +34,172 @@ 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. +/** + * 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 DiskSpaceCalculator { - private File mFile = null; - private long mBytesPerSecond = -1; - private long mBytesRemaining; + +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; - private long mStartTime; - private long mStartBytesRemaining; + // State for tracking file size of recording. + private File mRecordingFile; + private long mMaxBytes; - private long mPollTime; + // Rate at which the file grows + private int mBytesPerSecond; - public DiskSpaceCalculator() { - mFile = Environment.getExternalStorageDirectory(); + // 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() { - mBytesPerSecond = -1; - mBytesRemaining = -1; - mStartBytesRemaining = -1; + mCurrentLowerLimit = UNKNOWN_LIMIT; + mBlocksChangedTime = -1; + mFileSizeChangedTime = -1; } - /* - * Updates mBytesPerSecond, returns ammount of disk space available + /** + * Returns how long (in seconds) we can continue recording. */ - public long pollDiskSpace() { - StatFs fs = new StatFs(mFile.getAbsolutePath()); - long numBlocks = fs.getAvailableBlocks(); + 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 b = numBlocks * blockSize; - long t = System.currentTimeMillis(); + long now = System.currentTimeMillis(); - if (b == mBytesRemaining) - return b; // nothing changed, don't recalculate mBytePerSecond + 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. */ - mPollTime = t; - mBytesRemaining = b; + // at mBlocksChangedTime we had this much time + long result = mLastBlocks*blockSize/mBytesPerSecond; + // so now we have this much time + result -= (now - mBlocksChangedTime)/1000; - if (mBytesRemaining > mStartBytesRemaining) { - // first call or space got freed up, reset the calculation - mStartBytesRemaining = mBytesRemaining; - mStartTime = mPollTime; - return b; + 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 size = mStartBytesRemaining - mBytesRemaining; - long time = (mPollTime - mStartTime)/1000; - if (time > 0) - mBytesPerSecond = size/time; + long result2 = (mMaxBytes - fileSize)/mBytesPerSecond; + result2 -= (now - mFileSizeChangedTime)/1000; + result2 -= 1; // just for safety - return b; + mCurrentLowerLimit = result < result2 + ? DISK_SPACE_LIMIT : FILE_SIZE_LIMIT; + + return Math.min(result, result2); } - public long timeRemaining() { - if (mBytesPerSecond <= 0) - return -1; - - long bytes = mBytesRemaining - mBytesPerSecond*(System.currentTimeMillis() - mPollTime)/1000; - return bytes/mBytesPerSecond; + /** + * 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 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"; - + 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. - DiskSpaceCalculator mDiskSpaceCalculator; + long mMaxFileSize = -1; // can be specified in the intent + RemainingTimeCalculator mRemainingTimeCalculator; String mTimerFormat; final Handler mHandler = new Handler(); @@ -129,26 +221,54 @@ public class SoundRecorder extends Activity 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); - mDiskSpaceCalculator = new DiskSpaceCalculator(); + 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); } } @@ -175,10 +295,15 @@ public class SoundRecorder extends Activity 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); @@ -206,22 +331,39 @@ public class SoundRecorder extends Activity mVUMeter.setRecorder(mRecorder); } + /* + * 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 (mDiskSpaceCalculator.pollDiskSpace() < 1024) { + } else if (!mRemainingTimeCalculator.diskSpaceAvailable()) { mSampleInterrupted = true; mErrorUiMessage = getResources().getString(R.string.storage_is_full); updateUi(); } else { - mRecorder.startRecording(); + 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: @@ -242,6 +384,9 @@ public class SoundRecorder extends Activity } } + /* + * Handle the "back" hardware key. + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { @@ -279,16 +424,64 @@ public class SoundRecorder extends Activity 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 = this.addToMediaDB(mRecorder.sampleFile()); - if (uri == null) + 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) { @@ -384,8 +577,7 @@ public class SoundRecorder extends Activity 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.MIME_TYPE, mRequestedType); cv.put(MediaStore.Audio.Media.ARTIST, res.getString(R.string.audio_db_artist_name)); cv.put(MediaStore.Audio.Media.ALBUM, @@ -417,7 +609,8 @@ public class SoundRecorder extends Activity } /** - * This is the big MM:SS timer. + * Update the big MM:SS timer. If we are in playback, also update the + * progress bar. */ private void updateTimerView() { Resources res = getResources(); @@ -439,20 +632,32 @@ public class SoundRecorder extends Activity 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() { - mDiskSpaceCalculator.pollDiskSpace(); - long t = mDiskSpaceCalculator.timeRemaining(); + long t = mRemainingTimeCalculator.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); + + 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; } @@ -579,18 +784,28 @@ public class SoundRecorder extends Activity 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) - mDiskSpaceCalculator.reset(); + 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(); |