summaryrefslogtreecommitdiff
path: root/src/com/android/soundrecorder/SoundRecorder.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commite2118f54af4c5215bd988979769e383292b9c9cb (patch)
tree5d9a6fd510c579ea0506bd68bdbaf0d76989137b /src/com/android/soundrecorder/SoundRecorder.java
downloadSoundRecorder-e2118f54af4c5215bd988979769e383292b9c9cb.tar.gz
Diffstat (limited to 'src/com/android/soundrecorder/SoundRecorder.java')
-rw-r--r--src/com/android/soundrecorder/SoundRecorder.java615
1 files changed, 615 insertions, 0 deletions
diff --git a/src/com/android/soundrecorder/SoundRecorder.java b/src/com/android/soundrecorder/SoundRecorder.java
new file mode 100644
index 0000000..a06815f
--- /dev/null
+++ b/src/com/android/soundrecorder/SoundRecorder.java
@@ -0,0 +1,615 @@
+package com.android.soundrecorder;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.StatFs;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/*
+ * The file grows in jumps every five seconds or so. This class interpolates in between the jumps
+ * so we get a smooth countdown.
+ */
+class DiskSpaceCalculator {
+ private File mFile = null;
+ private long mBytesPerSecond = -1;
+ private long mBytesRemaining;
+
+ private long mStartTime;
+ private long mStartBytesRemaining;
+
+ private long mPollTime;
+
+ public DiskSpaceCalculator() {
+ mFile = Environment.getExternalStorageDirectory();
+ }
+
+ public void reset() {
+ mBytesPerSecond = -1;
+ mBytesRemaining = -1;
+ mStartBytesRemaining = -1;
+ }
+
+ /*
+ * Updates mBytesPerSecond, returns ammount of disk space available
+ */
+ public long pollDiskSpace() {
+ StatFs fs = new StatFs(mFile.getAbsolutePath());
+ long numBlocks = fs.getAvailableBlocks();
+ long blockSize = fs.getBlockSize();
+ long b = numBlocks * blockSize;
+ long t = System.currentTimeMillis();
+
+ if (b == mBytesRemaining)
+ return b; // nothing changed, don't recalculate mBytePerSecond
+
+ mPollTime = t;
+ mBytesRemaining = b;
+
+ if (mBytesRemaining > mStartBytesRemaining) {
+ // first call or space got freed up, reset the calculation
+ mStartBytesRemaining = mBytesRemaining;
+ mStartTime = mPollTime;
+ return b;
+ }
+
+ long size = mStartBytesRemaining - mBytesRemaining;
+ long time = (mPollTime - mStartTime)/1000;
+ if (time > 0)
+ mBytesPerSecond = size/time;
+
+ return b;
+ }
+
+ public long timeRemaining() {
+ if (mBytesPerSecond <= 0)
+ return -1;
+
+ long bytes = mBytesRemaining - mBytesPerSecond*(System.currentTimeMillis() - mPollTime)/1000;
+ return bytes/mBytesPerSecond;
+ }
+
+}
+
+public class SoundRecorder extends Activity
+ implements Button.OnClickListener, Recorder.OnStateChangedListener {
+ static final String TAG = "SoundRecorder";
+ static final String STATE_FILE_NAME = "soundrecorder.state";
+ static final String MIME_TYPE = "audio/amr"; // this is a lie. See comment in com.google.android.mms.pdu.PduPersister
+ static final String RECORDER_STATE_KEY = "recorder_state";
+ static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted";
+
+ Recorder mRecorder;
+ boolean mSampleInterrupted = false;
+ String mErrorUiMessage = null; // Some error messages are displayed in the UI,
+ // not a dialog. This happens when a recording
+ // is interrupted for some reason.
+
+ DiskSpaceCalculator mDiskSpaceCalculator;
+
+ String mTimerFormat;
+ final Handler mHandler = new Handler();
+ Runnable mUpdateTimer = new Runnable() {
+ public void run() { updateTimerView(); }
+ };
+
+ ImageButton mRecordButton;
+ ImageButton mPlayButton;
+ ImageButton mStopButton;
+
+ ImageView mStateLED;
+ TextView mStateMessage1;
+ TextView mStateMessage2;
+ ProgressBar mStateProgressBar;
+ TextView mTimerView;
+
+ LinearLayout mExitButtons;
+ Button mAcceptButton;
+ Button mDiscardButton;
+ VUMeter mVUMeter;
+
+ @Override
+ public void onCreate(Bundle icycle) {
+ super.onCreate(icycle);
+
+ setContentView(R.layout.main);
+
+ mRecorder = new Recorder();
+ mRecorder.setOnStateChangedListener(this);
+ mDiskSpaceCalculator = new DiskSpaceCalculator();
+
+ initResourceRefs();
+
+ setResult(RESULT_CANCELED);
+
+ if (icycle != null) {
+ Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY);
+ if (recorderState != null) {
+ mRecorder.restoreState(recorderState);
+ mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false);
+ }
+ }
+
+ updateUi();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ setContentView(R.layout.main);
+ initResourceRefs();
+ updateUi();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (mRecorder.sampleLength() == 0)
+ return;
+
+ Bundle recorderState = new Bundle();
+
+ mRecorder.saveState(recorderState);
+ recorderState.putBoolean(SAMPLE_INTERRUPTED_KEY, mSampleInterrupted);
+
+ outState.putBundle(RECORDER_STATE_KEY, recorderState);
+ }
+
+ private void initResourceRefs() {
+ mRecordButton = (ImageButton) findViewById(R.id.recordButton);
+ mPlayButton = (ImageButton) findViewById(R.id.playButton);
+ mStopButton = (ImageButton) findViewById(R.id.stopButton);
+
+ mStateLED = (ImageView) findViewById(R.id.stateLED);
+ mStateMessage1 = (TextView) findViewById(R.id.stateMessage1);
+ mStateMessage2 = (TextView) findViewById(R.id.stateMessage2);
+ mStateProgressBar = (ProgressBar) findViewById(R.id.stateProgressBar);
+ mTimerView = (TextView) findViewById(R.id.timerView);
+
+ mExitButtons = (LinearLayout) findViewById(R.id.exitButtons);
+ mAcceptButton = (Button) findViewById(R.id.acceptButton);
+ mDiscardButton = (Button) findViewById(R.id.discardButton);
+ mVUMeter = (VUMeter) findViewById(R.id.uvMeter);
+
+ mRecordButton.setOnClickListener(this);
+ mPlayButton.setOnClickListener(this);
+ mStopButton.setOnClickListener(this);
+ mAcceptButton.setOnClickListener(this);
+ mDiscardButton.setOnClickListener(this);
+
+ mTimerFormat = getResources().getString(R.string.timer_format);
+
+ mVUMeter.setRecorder(mRecorder);
+ }
+
+ public void onClick(View button) {
+ if (!button.isEnabled())
+ return;
+
+ switch (button.getId()) {
+ case R.id.recordButton:
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.insert_sd_card);
+ updateUi();
+ } else if (mDiskSpaceCalculator.pollDiskSpace() < 1024) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.storage_is_full);
+ updateUi();
+ } else {
+ mRecorder.startRecording();
+ }
+ break;
+ case R.id.playButton:
+ mRecorder.startPlayback();
+ break;
+ case R.id.stopButton:
+ mRecorder.stop();
+ break;
+ case R.id.acceptButton:
+ mRecorder.stop();
+ saveSample();
+ finish();
+ break;
+ case R.id.discardButton:
+ mRecorder.delete();
+ finish();
+ break;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ switch (mRecorder.state()) {
+ case Recorder.IDLE_STATE:
+ if (mRecorder.sampleLength() > 0)
+ saveSample();
+ finish();
+ break;
+ case Recorder.PLAYING_STATE:
+ mRecorder.stop();
+ saveSample();
+ break;
+ case Recorder.RECORDING_STATE:
+ mRecorder.clear();
+ break;
+ }
+ return true;
+ } else {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ mRecorder.stop();
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ mSampleInterrupted = mRecorder.state() == Recorder.RECORDING_STATE;
+ mRecorder.stop();
+
+ super.onPause();
+ }
+
+ private void saveSample() {
+ if (mRecorder.sampleLength() == 0)
+ return;
+ Uri uri = this.addToMediaDB(mRecorder.sampleFile());
+ if (uri == null)
+ return;
+ setResult(RESULT_OK, new Intent().setData(uri));
+ }
+
+ /*
+ * A simple utility to do a query into the databases.
+ */
+ private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ try {
+ ContentResolver resolver = getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ /*
+ * Add the given audioId to the playlist with the given playlistId; and maintain the
+ * play_order in the playlist.
+ */
+ private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) {
+ String[] cols = new String[] {
+ "count(*)"
+ };
+ Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
+ Cursor cur = resolver.query(uri, cols, null, null, null);
+ cur.moveToFirst();
+ final int base = cur.getInt(0);
+ cur.close();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId));
+ values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
+ resolver.insert(uri, values);
+ }
+
+ /*
+ * Obtain the id for the default play list from the audio_playlists table.
+ */
+ private int getPlaylistId(Resources res) {
+ Uri uri = MediaStore.Audio.Playlists.getContentUri("external");
+ final String[] ids = new String[] { MediaStore.Audio.Playlists._ID };
+ final String where = MediaStore.Audio.Playlists.NAME + "=?";
+ final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) };
+ Cursor cursor = query(uri, ids, where, args, null);
+ if (cursor == null) {
+ Log.v(TAG, "query returns null");
+ }
+ int id = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
+ }
+ cursor.close();
+ return id;
+ }
+
+ /*
+ * Create a playlist with the given default playlist name, if no such playlist exists.
+ */
+ private Uri createPlaylist(Resources res, ContentResolver resolver) {
+ ContentValues cv = new ContentValues();
+ cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name));
+ Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
+ if (uri == null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(R.string.error_mediadb_new_record)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ }
+ return uri;
+ }
+
+ /*
+ * Adds file and returns content uri.
+ */
+ private Uri addToMediaDB(File file) {
+ Resources res = getResources();
+ ContentValues cv = new ContentValues();
+ long current = System.currentTimeMillis();
+ long modDate = file.lastModified();
+ Date date = new Date(current);
+ SimpleDateFormat formatter = new SimpleDateFormat(
+ res.getString(R.string.audio_db_title_format));
+ String title = formatter.format(date);
+
+ // Lets label the recorded audio file as NON-MUSIC so that the file
+ // won't be displayed automatically, except for in the playlist.
+ cv.put(MediaStore.Audio.Media.IS_MUSIC, "0");
+
+ cv.put(MediaStore.Audio.Media.TITLE, title);
+ cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
+ cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000));
+ cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000));
+// cv.put(MediaStore.Audio.MediaColumns.DURATION, mRecordingLength);
+ cv.put(MediaStore.Audio.Media.MIME_TYPE, MIME_TYPE);
+ cv.put(MediaStore.Audio.Media.ARTIST,
+ res.getString(R.string.audio_db_artist_name));
+ cv.put(MediaStore.Audio.Media.ALBUM,
+ res.getString(R.string.audio_db_album_name));
+ Log.d(TAG, "Inserting audio record: " + cv.toString());
+ ContentResolver resolver = getContentResolver();
+ Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ Log.d(TAG, "ContentURI: " + base);
+ Uri result = resolver.insert(base, cv);
+ if (result == null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(R.string.error_mediadb_new_record)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ return null;
+ }
+ if (getPlaylistId(res) == -1) {
+ createPlaylist(res, resolver);
+ }
+ int audioId = Integer.valueOf(result.getLastPathSegment());
+ addToPlaylist(resolver, audioId, getPlaylistId(res));
+
+ // Notify those applications such as Music listening to the
+ // scanner events that a recorded audio file just created.
+ sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
+ return result;
+ }
+
+ /**
+ * This is the big MM:SS timer.
+ */
+ private void updateTimerView() {
+ Resources res = getResources();
+ int state = mRecorder.state();
+
+ boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE;
+
+ long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength();
+ String timeStr = String.format(mTimerFormat, time/60, time%60);
+ mTimerView.setText(timeStr);
+
+ if (state == Recorder.PLAYING_STATE) {
+ mStateProgressBar.setProgress((int)(100*time/mRecorder.sampleLength()));
+ } else if (state == Recorder.RECORDING_STATE) {
+ updateTimeRemaining();
+ }
+
+ if (ongoing)
+ mHandler.postDelayed(mUpdateTimer, 1000);
+ }
+
+ private void updateTimeRemaining() {
+ mDiskSpaceCalculator.pollDiskSpace();
+ long t = mDiskSpaceCalculator.timeRemaining();
+
+ if (t == -1) {
+ mStateMessage1.setText("");
+ return;
+ }
+
+ t -= 5; // safety buffer of 5 secs
+
+ if (t <= 0) {
+ mSampleInterrupted = true;
+ mErrorUiMessage = getResources().getString(R.string.storage_is_full);
+ mRecorder.stop();
+ return;
+ }
+
+ Resources res = getResources();
+ String timeStr = "";
+
+ if (t < 60)
+ timeStr = String.format(res.getString(R.string.sec_available), t);
+ else if (t < 540)
+ timeStr = String.format(res.getString(R.string.min_available), t/60 + 1);
+
+ mStateMessage1.setText(timeStr);
+ }
+
+ /**
+ * Shows/hides the appropriate child views for the new state.
+ */
+ private void updateUi() {
+ Resources res = getResources();
+
+ switch (mRecorder.state()) {
+ case Recorder.IDLE_STATE:
+ if (mRecorder.sampleLength() == 0) {
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(false);
+ mStopButton.setFocusable(false);
+ mRecordButton.requestFocus();
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.VISIBLE);
+ mStateLED.setImageResource(R.drawable.idle_led);
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.press_record));
+
+ mExitButtons.setVisibility(View.INVISIBLE);
+ mVUMeter.setVisibility(View.VISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.record_your_message));
+ } else {
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(true);
+ mPlayButton.setFocusable(true);
+ mStopButton.setEnabled(false);
+ mStopButton.setFocusable(false);
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.INVISIBLE);
+ mStateMessage2.setVisibility(View.INVISIBLE);
+
+ mExitButtons.setVisibility(View.VISIBLE);
+ mVUMeter.setVisibility(View.INVISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.message_recorded));
+ }
+
+ if (mSampleInterrupted) {
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.recording_stopped));
+ mStateLED.setImageResource(R.drawable.idle_led);
+ mStateLED.setVisibility(View.VISIBLE);
+ }
+
+ if (mErrorUiMessage != null) {
+ mStateMessage1.setText(mErrorUiMessage);
+ mStateMessage1.setVisibility(View.VISIBLE);
+ }
+
+ break;
+ case Recorder.RECORDING_STATE:
+ mRecordButton.setEnabled(false);
+ mRecordButton.setFocusable(false);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(true);
+ mStopButton.setFocusable(true);
+
+ mStateMessage1.setVisibility(View.VISIBLE);
+ mStateLED.setVisibility(View.VISIBLE);
+ mStateLED.setImageResource(R.drawable.recording_led);
+ mStateMessage2.setVisibility(View.VISIBLE);
+ mStateMessage2.setText(res.getString(R.string.recording));
+
+ mExitButtons.setVisibility(View.INVISIBLE);
+ mVUMeter.setVisibility(View.VISIBLE);
+
+ mStateProgressBar.setVisibility(View.INVISIBLE);
+
+ setTitle(res.getString(R.string.record_your_message));
+
+ break;
+
+ case Recorder.PLAYING_STATE:
+ mRecordButton.setEnabled(true);
+ mRecordButton.setFocusable(true);
+ mPlayButton.setEnabled(false);
+ mPlayButton.setFocusable(false);
+ mStopButton.setEnabled(true);
+ mStopButton.setFocusable(true);
+
+ mStateMessage1.setVisibility(View.INVISIBLE);
+ mStateLED.setVisibility(View.INVISIBLE);
+ mStateMessage2.setVisibility(View.INVISIBLE);
+
+ mExitButtons.setVisibility(View.VISIBLE);
+ mVUMeter.setVisibility(View.INVISIBLE);
+
+ mStateProgressBar.setVisibility(View.VISIBLE);
+
+ setTitle(res.getString(R.string.review_message));
+
+ break;
+ }
+
+ updateTimerView();
+ mVUMeter.invalidate();
+ }
+
+ public void onStateChanged(int state) {
+ if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) {
+ mSampleInterrupted = false;
+ mErrorUiMessage = null;
+ }
+
+ if (state == Recorder.RECORDING_STATE)
+ mDiskSpaceCalculator.reset();
+
+ updateUi();
+ }
+
+ public void onError(int error) {
+ Resources res = getResources();
+
+ String message = null;
+ switch (error) {
+ case Recorder.SDCARD_ACCESS_ERROR:
+ message = res.getString(R.string.error_sdcard_access);
+ break;
+ case Recorder.INTERNAL_ERROR:
+ message = res.getString(R.string.error_app_internal);
+ break;
+ }
+ if (message != null) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.app_name)
+ .setMessage(message)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(false)
+ .show();
+ }
+ }
+}