summaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-12-17 18:06:01 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2008-12-17 18:06:01 -0800
commitcefd3e81da22167a56c89a8794c9b8dfe67a7673 (patch)
treefea078b53cf59b9688700747527265af91fdc53f /src/com
parente2118f54af4c5215bd988979769e383292b9c9cb (diff)
downloadSoundRecorder-cefd3e81da22167a56c89a8794c9b8dfe67a7673.tar.gz
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/soundrecorder/Recorder.java20
-rw-r--r--src/com/android/soundrecorder/SoundRecorder.java349
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();