diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | e2118f54af4c5215bd988979769e383292b9c9cb (patch) | |
tree | 5d9a6fd510c579ea0506bd68bdbaf0d76989137b /src/com/android/soundrecorder/SoundRecorder.java | |
download | SoundRecorder-e2118f54af4c5215bd988979769e383292b9c9cb.tar.gz |
Initial Contributionandroid-1.0release-1.0cdma-import
Diffstat (limited to 'src/com/android/soundrecorder/SoundRecorder.java')
-rw-r--r-- | src/com/android/soundrecorder/SoundRecorder.java | 615 |
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(); + } + } +} |