SoundRecorder.java revision 27a66070449c91de9177fe6a9ac48381608c517d
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.soundrecorder;
18
19import java.io.File;
20import java.text.SimpleDateFormat;
21import java.util.Date;
22
23import android.app.Activity;
24import android.app.AlertDialog;
25import android.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.Intent;
28import android.content.Context;
29import android.content.IntentFilter;
30import android.content.BroadcastReceiver;
31import android.content.res.Configuration;
32import android.content.res.Resources;
33import android.database.Cursor;
34import android.media.MediaRecorder;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Environment;
38import android.os.Handler;
39import android.os.PowerManager;
40import android.os.StatFs;
41import android.os.PowerManager.WakeLock;
42import android.provider.MediaStore;
43import android.util.Log;
44import android.view.KeyEvent;
45import android.view.View;
46import android.widget.Button;
47import android.widget.ImageButton;
48import android.widget.ImageView;
49import android.widget.LinearLayout;
50import android.widget.ProgressBar;
51import android.widget.TextView;
52
53/**
54 * Calculates remaining recording time based on available disk space and
55 * optionally a maximum recording file size.
56 *
57 * The reason why this is not trivial is that the file grows in blocks
58 * every few seconds or so, while we want a smooth countdown.
59 */
60
61class RemainingTimeCalculator {
62    public static final int UNKNOWN_LIMIT = 0;
63    public static final int FILE_SIZE_LIMIT = 1;
64    public static final int DISK_SPACE_LIMIT = 2;
65
66    // which of the two limits we will hit (or have fit) first
67    private int mCurrentLowerLimit = UNKNOWN_LIMIT;
68
69    private File mSDCardDirectory;
70
71     // State for tracking file size of recording.
72    private File mRecordingFile;
73    private long mMaxBytes;
74
75    // Rate at which the file grows
76    private int mBytesPerSecond;
77
78    // time at which number of free blocks last changed
79    private long mBlocksChangedTime;
80    // number of available blocks at that time
81    private long mLastBlocks;
82
83    // time at which the size of the file has last changed
84    private long mFileSizeChangedTime;
85    // size of the file at that time
86    private long mLastFileSize;
87
88    public RemainingTimeCalculator() {
89        mSDCardDirectory = Environment.getExternalStorageDirectory();
90    }
91
92    /**
93     * If called, the calculator will return the minimum of two estimates:
94     * how long until we run out of disk space and how long until the file
95     * reaches the specified size.
96     *
97     * @param file the file to watch
98     * @param maxBytes the limit
99     */
100
101    public void setFileSizeLimit(File file, long maxBytes) {
102        mRecordingFile = file;
103        mMaxBytes = maxBytes;
104    }
105
106    /**
107     * Resets the interpolation.
108     */
109    public void reset() {
110        mCurrentLowerLimit = UNKNOWN_LIMIT;
111        mBlocksChangedTime = -1;
112        mFileSizeChangedTime = -1;
113    }
114
115    /**
116     * Returns how long (in seconds) we can continue recording.
117     */
118    public long timeRemaining() {
119        // Calculate how long we can record based on free disk space
120
121        StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
122        long blocks = fs.getAvailableBlocks();
123        long blockSize = fs.getBlockSize();
124        long now = System.currentTimeMillis();
125
126        if (mBlocksChangedTime == -1 || blocks != mLastBlocks) {
127            mBlocksChangedTime = now;
128            mLastBlocks = blocks;
129        }
130
131        /* The calculation below always leaves one free block, since free space
132           in the block we're currently writing to is not added. This
133           last block might get nibbled when we close and flush the file, but
134           we won't run out of disk. */
135
136        // at mBlocksChangedTime we had this much time
137        long result = mLastBlocks*blockSize/mBytesPerSecond;
138        // so now we have this much time
139        result -= (now - mBlocksChangedTime)/1000;
140
141        if (mRecordingFile == null) {
142            mCurrentLowerLimit = DISK_SPACE_LIMIT;
143            return result;
144        }
145
146        // If we have a recording file set, we calculate a second estimate
147        // based on how long it will take us to reach mMaxBytes.
148
149        mRecordingFile = new File(mRecordingFile.getAbsolutePath());
150        long fileSize = mRecordingFile.length();
151        if (mFileSizeChangedTime == -1 || fileSize != mLastFileSize) {
152            mFileSizeChangedTime = now;
153            mLastFileSize = fileSize;
154        }
155
156        long result2 = (mMaxBytes - fileSize)/mBytesPerSecond;
157        result2 -= (now - mFileSizeChangedTime)/1000;
158        result2 -= 1; // just for safety
159
160        mCurrentLowerLimit = result < result2
161            ? DISK_SPACE_LIMIT : FILE_SIZE_LIMIT;
162
163        return Math.min(result, result2);
164    }
165
166    /**
167     * Indicates which limit we will hit (or have hit) first, by returning one
168     * of FILE_SIZE_LIMIT or DISK_SPACE_LIMIT or UNKNOWN_LIMIT. We need this to
169     * display the correct message to the user when we hit one of the limits.
170     */
171    public int currentLowerLimit() {
172        return mCurrentLowerLimit;
173    }
174
175    /**
176     * Is there any point of trying to start recording?
177     */
178    public boolean diskSpaceAvailable() {
179        StatFs fs = new StatFs(mSDCardDirectory.getAbsolutePath());
180        // keep one free block
181        return fs.getAvailableBlocks() > 1;
182    }
183
184    /**
185     * Sets the bit rate used in the interpolation.
186     *
187     * @param bitRate the bit rate to set in bits/sec.
188     */
189    public void setBitRate(int bitRate) {
190        mBytesPerSecond = bitRate/8;
191    }
192}
193
194public class SoundRecorder extends Activity
195        implements Button.OnClickListener, Recorder.OnStateChangedListener {
196    static final String TAG = "SoundRecorder";
197    static final String STATE_FILE_NAME = "soundrecorder.state";
198    static final String RECORDER_STATE_KEY = "recorder_state";
199    static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted";
200    static final String MAX_FILE_SIZE_KEY = "max_file_size";
201
202    static final String AUDIO_3GPP = "audio/3gpp";
203    static final String AUDIO_AMR = "audio/amr";
204    static final String AUDIO_ANY = "audio/*";
205    static final String ANY_ANY = "*/*";
206
207    static final int BITRATE_AMR =  5900; // bits/sec
208    static final int BITRATE_3GPP = 5900;
209
210    WakeLock mWakeLock;
211    String mRequestedType = AUDIO_ANY;
212    Recorder mRecorder;
213    boolean mSampleInterrupted = false;
214    String mErrorUiMessage = null; // Some error messages are displayed in the UI,
215                                   // not a dialog. This happens when a recording
216                                   // is interrupted for some reason.
217
218    long mMaxFileSize = -1;        // can be specified in the intent
219    RemainingTimeCalculator mRemainingTimeCalculator;
220
221    String mTimerFormat;
222    final Handler mHandler = new Handler();
223    Runnable mUpdateTimer = new Runnable() {
224        public void run() { updateTimerView(); }
225    };
226
227    ImageButton mRecordButton;
228    ImageButton mPlayButton;
229    ImageButton mStopButton;
230
231    ImageView mStateLED;
232    TextView mStateMessage1;
233    TextView mStateMessage2;
234    ProgressBar mStateProgressBar;
235    TextView mTimerView;
236
237    LinearLayout mExitButtons;
238    Button mAcceptButton;
239    Button mDiscardButton;
240    VUMeter mVUMeter;
241    private BroadcastReceiver mSDCardMountEventReceiver = null;
242
243    @Override
244    public void onCreate(Bundle icycle) {
245        super.onCreate(icycle);
246
247        Intent i = getIntent();
248        if (i != null) {
249            String s = i.getType();
250            if (AUDIO_AMR.equals(s) || AUDIO_3GPP.equals(s) || AUDIO_ANY.equals(s)
251                    || ANY_ANY.equals(s)) {
252                mRequestedType = s;
253            } else if (s != null) {
254                // we only support amr and 3gpp formats right now
255                setResult(RESULT_CANCELED);
256                finish();
257                return;
258            }
259
260            final String EXTRA_MAX_BYTES
261                = android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES;
262            mMaxFileSize = i.getLongExtra(EXTRA_MAX_BYTES, -1);
263        }
264
265        if (AUDIO_ANY.equals(mRequestedType) || ANY_ANY.equals(mRequestedType)) {
266            mRequestedType = AUDIO_3GPP;
267        }
268
269        setContentView(R.layout.main);
270
271        mRecorder = new Recorder();
272        mRecorder.setOnStateChangedListener(this);
273        mRemainingTimeCalculator = new RemainingTimeCalculator();
274
275        PowerManager pm
276            = (PowerManager) getSystemService(Context.POWER_SERVICE);
277        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK,
278                                    "SoundRecorder");
279
280        initResourceRefs();
281
282        setResult(RESULT_CANCELED);
283        registerExternalStorageListener();
284        if (icycle != null) {
285            Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY);
286            if (recorderState != null) {
287                mRecorder.restoreState(recorderState);
288                mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false);
289                mMaxFileSize = recorderState.getLong(MAX_FILE_SIZE_KEY, -1);
290            }
291        }
292
293        updateUi();
294    }
295
296    @Override
297    public void onConfigurationChanged(Configuration newConfig) {
298        super.onConfigurationChanged(newConfig);
299
300        setContentView(R.layout.main);
301        initResourceRefs();
302        updateUi();
303    }
304
305    @Override
306    protected void onSaveInstanceState(Bundle outState) {
307        super.onSaveInstanceState(outState);
308
309        if (mRecorder.sampleLength() == 0)
310            return;
311
312        Bundle recorderState = new Bundle();
313
314        mRecorder.saveState(recorderState);
315        recorderState.putBoolean(SAMPLE_INTERRUPTED_KEY, mSampleInterrupted);
316        recorderState.putLong(MAX_FILE_SIZE_KEY, mMaxFileSize);
317
318        outState.putBundle(RECORDER_STATE_KEY, recorderState);
319    }
320
321    /*
322     * Whenever the UI is re-created (due f.ex. to orientation change) we have
323     * to reinitialize references to the views.
324     */
325    private void initResourceRefs() {
326        mRecordButton = (ImageButton) findViewById(R.id.recordButton);
327        mPlayButton = (ImageButton) findViewById(R.id.playButton);
328        mStopButton = (ImageButton) findViewById(R.id.stopButton);
329
330        mStateLED = (ImageView) findViewById(R.id.stateLED);
331        mStateMessage1 = (TextView) findViewById(R.id.stateMessage1);
332        mStateMessage2 = (TextView) findViewById(R.id.stateMessage2);
333        mStateProgressBar = (ProgressBar) findViewById(R.id.stateProgressBar);
334        mTimerView = (TextView) findViewById(R.id.timerView);
335
336        mExitButtons = (LinearLayout) findViewById(R.id.exitButtons);
337        mAcceptButton = (Button) findViewById(R.id.acceptButton);
338        mDiscardButton = (Button) findViewById(R.id.discardButton);
339        mVUMeter = (VUMeter) findViewById(R.id.uvMeter);
340
341        mRecordButton.setOnClickListener(this);
342        mPlayButton.setOnClickListener(this);
343        mStopButton.setOnClickListener(this);
344        mAcceptButton.setOnClickListener(this);
345        mDiscardButton.setOnClickListener(this);
346
347        mTimerFormat = getResources().getString(R.string.timer_format);
348
349        mVUMeter.setRecorder(mRecorder);
350    }
351
352    /*
353     * Make sure we're not recording music playing in the background, ask
354     * the MediaPlaybackService to pause playback.
355     */
356    private void stopAudioPlayback() {
357        // Shamelessly copied from MediaPlaybackService.java, which
358        // should be public, but isn't.
359        Intent i = new Intent("com.android.music.musicservicecommand");
360        i.putExtra("command", "pause");
361
362        sendBroadcast(i);
363    }
364
365    /*
366     * Handle the buttons.
367     */
368    public void onClick(View button) {
369        if (!button.isEnabled())
370            return;
371
372        switch (button.getId()) {
373            case R.id.recordButton:
374                mRemainingTimeCalculator.reset();
375                if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
376                    mSampleInterrupted = true;
377                    mErrorUiMessage = getResources().getString(R.string.insert_sd_card);
378                    updateUi();
379                } else if (!mRemainingTimeCalculator.diskSpaceAvailable()) {
380                    mSampleInterrupted = true;
381                    mErrorUiMessage = getResources().getString(R.string.storage_is_full);
382                    updateUi();
383                } else {
384                    stopAudioPlayback();
385
386                    if (AUDIO_AMR.equals(mRequestedType)) {
387                        mRemainingTimeCalculator.setBitRate(BITRATE_AMR);
388                        mRecorder.startRecording(MediaRecorder.OutputFormat.AMR_NB, ".amr", this);
389                    } else if (AUDIO_3GPP.equals(mRequestedType)) {
390                        mRemainingTimeCalculator.setBitRate(BITRATE_3GPP);
391                        mRecorder.startRecording(MediaRecorder.OutputFormat.THREE_GPP, ".3gpp",
392                                this);
393                    } else {
394                        throw new IllegalArgumentException("Invalid output file type requested");
395                    }
396
397                    if (mMaxFileSize != -1) {
398                        mRemainingTimeCalculator.setFileSizeLimit(
399                                mRecorder.sampleFile(), mMaxFileSize);
400                    }
401                }
402                break;
403            case R.id.playButton:
404                mRecorder.startPlayback();
405                break;
406            case R.id.stopButton:
407                mRecorder.stop();
408                break;
409            case R.id.acceptButton:
410                mRecorder.stop();
411                saveSample();
412                finish();
413                break;
414            case R.id.discardButton:
415                mRecorder.delete();
416                finish();
417                break;
418        }
419    }
420
421    /*
422     * Handle the "back" hardware key.
423     */
424    @Override
425    public boolean onKeyDown(int keyCode, KeyEvent event) {
426        if (keyCode == KeyEvent.KEYCODE_BACK) {
427            switch (mRecorder.state()) {
428                case Recorder.IDLE_STATE:
429                    if (mRecorder.sampleLength() > 0)
430                        saveSample();
431                    finish();
432                    break;
433                case Recorder.PLAYING_STATE:
434                    mRecorder.stop();
435                    saveSample();
436                    break;
437                case Recorder.RECORDING_STATE:
438                    mRecorder.clear();
439                    break;
440            }
441            return true;
442        } else {
443            return super.onKeyDown(keyCode, event);
444        }
445    }
446
447    @Override
448    public void onStop() {
449        mRecorder.stop();
450        super.onStop();
451    }
452
453    @Override
454    protected void onPause() {
455        mSampleInterrupted = mRecorder.state() == Recorder.RECORDING_STATE;
456        mRecorder.stop();
457
458        super.onPause();
459    }
460
461    /*
462     * If we have just recorded a smaple, this adds it to the media data base
463     * and sets the result to the sample's URI.
464     */
465    private void saveSample() {
466        if (mRecorder.sampleLength() == 0)
467            return;
468        Uri uri = null;
469        try {
470            uri = this.addToMediaDB(mRecorder.sampleFile());
471        } catch(UnsupportedOperationException ex) {  // Database manipulation failure
472            return;
473        }
474        if (uri == null) {
475            return;
476        }
477        setResult(RESULT_OK, new Intent().setData(uri));
478    }
479
480    /*
481     * Called on destroy to unregister the SD card mount event receiver.
482     */
483    @Override
484    public void onDestroy() {
485        if (mSDCardMountEventReceiver != null) {
486            unregisterReceiver(mSDCardMountEventReceiver);
487            mSDCardMountEventReceiver = null;
488        }
489        super.onDestroy();
490    }
491
492    /*
493     * Registers an intent to listen for ACTION_MEDIA_EJECT/ACTION_MEDIA_MOUNTED
494     * notifications.
495     */
496    private void registerExternalStorageListener() {
497        if (mSDCardMountEventReceiver == null) {
498            mSDCardMountEventReceiver = new BroadcastReceiver() {
499                @Override
500                public void onReceive(Context context, Intent intent) {
501                    String action = intent.getAction();
502                    if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
503                        mRecorder.delete();
504                    } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
505                        mSampleInterrupted = false;
506                        updateUi();
507                    }
508                }
509            };
510            IntentFilter iFilter = new IntentFilter();
511            iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
512            iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
513            iFilter.addDataScheme("file");
514            registerReceiver(mSDCardMountEventReceiver, iFilter);
515        }
516    }
517
518    /*
519     * A simple utility to do a query into the databases.
520     */
521    private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
522        try {
523            ContentResolver resolver = getContentResolver();
524            if (resolver == null) {
525                return null;
526            }
527            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
528         } catch (UnsupportedOperationException ex) {
529            return null;
530        }
531    }
532
533    /*
534     * Add the given audioId to the playlist with the given playlistId; and maintain the
535     * play_order in the playlist.
536     */
537    private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) {
538        String[] cols = new String[] {
539                "count(*)"
540        };
541        Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
542        Cursor cur = resolver.query(uri, cols, null, null, null);
543        cur.moveToFirst();
544        final int base = cur.getInt(0);
545        cur.close();
546        ContentValues values = new ContentValues();
547        values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId));
548        values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
549        resolver.insert(uri, values);
550    }
551
552    /*
553     * Obtain the id for the default play list from the audio_playlists table.
554     */
555    private int getPlaylistId(Resources res) {
556        Uri uri = MediaStore.Audio.Playlists.getContentUri("external");
557        final String[] ids = new String[] { MediaStore.Audio.Playlists._ID };
558        final String where = MediaStore.Audio.Playlists.NAME + "=?";
559        final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) };
560        Cursor cursor = query(uri, ids, where, args, null);
561        if (cursor == null) {
562            Log.v(TAG, "query returns null");
563        }
564        int id = -1;
565        if (cursor != null) {
566            cursor.moveToFirst();
567            if (!cursor.isAfterLast()) {
568                id = cursor.getInt(0);
569            }
570        }
571        cursor.close();
572        return id;
573    }
574
575    /*
576     * Create a playlist with the given default playlist name, if no such playlist exists.
577     */
578    private Uri createPlaylist(Resources res, ContentResolver resolver) {
579        ContentValues cv = new ContentValues();
580        cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name));
581        Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
582        if (uri == null) {
583            new AlertDialog.Builder(this)
584                .setTitle(R.string.app_name)
585                .setMessage(R.string.error_mediadb_new_record)
586                .setPositiveButton(R.string.button_ok, null)
587                .setCancelable(false)
588                .show();
589        }
590        return uri;
591    }
592
593    /*
594     * Adds file and returns content uri.
595     */
596    private Uri addToMediaDB(File file) {
597        Resources res = getResources();
598        ContentValues cv = new ContentValues();
599        long current = System.currentTimeMillis();
600        long modDate = file.lastModified();
601        Date date = new Date(current);
602        SimpleDateFormat formatter = new SimpleDateFormat(
603                res.getString(R.string.audio_db_title_format));
604        String title = formatter.format(date);
605        long sampleLengthMillis = mRecorder.sampleLength() * 1000L;
606
607        // Lets label the recorded audio file as NON-MUSIC so that the file
608        // won't be displayed automatically, except for in the playlist.
609        cv.put(MediaStore.Audio.Media.IS_MUSIC, "0");
610
611        cv.put(MediaStore.Audio.Media.TITLE, title);
612        cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
613        cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000));
614        cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000));
615        cv.put(MediaStore.Audio.Media.DURATION, sampleLengthMillis);
616        cv.put(MediaStore.Audio.Media.MIME_TYPE, mRequestedType);
617        cv.put(MediaStore.Audio.Media.ARTIST,
618                res.getString(R.string.audio_db_artist_name));
619        cv.put(MediaStore.Audio.Media.ALBUM,
620                res.getString(R.string.audio_db_album_name));
621        Log.d(TAG, "Inserting audio record: " + cv.toString());
622        ContentResolver resolver = getContentResolver();
623        Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
624        Log.d(TAG, "ContentURI: " + base);
625        Uri result = resolver.insert(base, cv);
626        if (result == null) {
627            new AlertDialog.Builder(this)
628                .setTitle(R.string.app_name)
629                .setMessage(R.string.error_mediadb_new_record)
630                .setPositiveButton(R.string.button_ok, null)
631                .setCancelable(false)
632                .show();
633            return null;
634        }
635        if (getPlaylistId(res) == -1) {
636            createPlaylist(res, resolver);
637        }
638        int audioId = Integer.valueOf(result.getLastPathSegment());
639        addToPlaylist(resolver, audioId, getPlaylistId(res));
640
641        // Notify those applications such as Music listening to the
642        // scanner events that a recorded audio file just created.
643        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
644        return result;
645    }
646
647    /**
648     * Update the big MM:SS timer. If we are in playback, also update the
649     * progress bar.
650     */
651    private void updateTimerView() {
652        Resources res = getResources();
653        int state = mRecorder.state();
654
655        boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE;
656
657        long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength();
658        String timeStr = String.format(mTimerFormat, time/60, time%60);
659        mTimerView.setText(timeStr);
660
661        if (state == Recorder.PLAYING_STATE) {
662            mStateProgressBar.setProgress((int)(100*time/mRecorder.sampleLength()));
663        } else if (state == Recorder.RECORDING_STATE) {
664            updateTimeRemaining();
665        }
666
667        if (ongoing)
668            mHandler.postDelayed(mUpdateTimer, 1000);
669    }
670
671    /*
672     * Called when we're in recording state. Find out how much longer we can
673     * go on recording. If it's under 5 minutes, we display a count-down in
674     * the UI. If we've run out of time, stop the recording.
675     */
676    private void updateTimeRemaining() {
677        long t = mRemainingTimeCalculator.timeRemaining();
678
679        if (t <= 0) {
680            mSampleInterrupted = true;
681
682            int limit = mRemainingTimeCalculator.currentLowerLimit();
683            switch (limit) {
684                case RemainingTimeCalculator.DISK_SPACE_LIMIT:
685                    mErrorUiMessage
686                        = getResources().getString(R.string.storage_is_full);
687                    break;
688                case RemainingTimeCalculator.FILE_SIZE_LIMIT:
689                    mErrorUiMessage
690                        = getResources().getString(R.string.max_length_reached);
691                    break;
692                default:
693                    mErrorUiMessage = null;
694                    break;
695            }
696
697            mRecorder.stop();
698            return;
699        }
700
701        Resources res = getResources();
702        String timeStr = "";
703
704        if (t < 60)
705            timeStr = String.format(res.getString(R.string.sec_available), t);
706        else if (t < 540)
707            timeStr = String.format(res.getString(R.string.min_available), t/60 + 1);
708
709        mStateMessage1.setText(timeStr);
710    }
711
712    /**
713     * Shows/hides the appropriate child views for the new state.
714     */
715    private void updateUi() {
716        Resources res = getResources();
717
718        switch (mRecorder.state()) {
719            case Recorder.IDLE_STATE:
720                if (mRecorder.sampleLength() == 0) {
721                    mRecordButton.setEnabled(true);
722                    mRecordButton.setFocusable(true);
723                    mPlayButton.setEnabled(false);
724                    mPlayButton.setFocusable(false);
725                    mStopButton.setEnabled(false);
726                    mStopButton.setFocusable(false);
727                    mRecordButton.requestFocus();
728
729                    mStateMessage1.setVisibility(View.INVISIBLE);
730                    mStateLED.setVisibility(View.INVISIBLE);
731                    mStateMessage2.setVisibility(View.INVISIBLE);
732
733                    mExitButtons.setVisibility(View.INVISIBLE);
734                    mVUMeter.setVisibility(View.VISIBLE);
735
736                    mStateProgressBar.setVisibility(View.INVISIBLE);
737
738                    setTitle(res.getString(R.string.record_your_message));
739                } else {
740                    mRecordButton.setEnabled(true);
741                    mRecordButton.setFocusable(true);
742                    mPlayButton.setEnabled(true);
743                    mPlayButton.setFocusable(true);
744                    mStopButton.setEnabled(false);
745                    mStopButton.setFocusable(false);
746
747                    mStateMessage1.setVisibility(View.INVISIBLE);
748                    mStateLED.setVisibility(View.INVISIBLE);
749                    mStateMessage2.setVisibility(View.INVISIBLE);
750
751                    mExitButtons.setVisibility(View.VISIBLE);
752                    mVUMeter.setVisibility(View.INVISIBLE);
753
754                    mStateProgressBar.setVisibility(View.INVISIBLE);
755
756                    setTitle(res.getString(R.string.message_recorded));
757                }
758
759                if (mSampleInterrupted) {
760                    mStateMessage2.setVisibility(View.VISIBLE);
761                    mStateMessage2.setText(res.getString(R.string.recording_stopped));
762                    mStateLED.setVisibility(View.INVISIBLE);
763                }
764
765                if (mErrorUiMessage != null) {
766                    mStateMessage1.setText(mErrorUiMessage);
767                    mStateMessage1.setVisibility(View.VISIBLE);
768                }
769
770                break;
771            case Recorder.RECORDING_STATE:
772                mRecordButton.setEnabled(false);
773                mRecordButton.setFocusable(false);
774                mPlayButton.setEnabled(false);
775                mPlayButton.setFocusable(false);
776                mStopButton.setEnabled(true);
777                mStopButton.setFocusable(true);
778
779                mStateMessage1.setVisibility(View.VISIBLE);
780                mStateLED.setVisibility(View.VISIBLE);
781                mStateLED.setImageResource(R.drawable.recording_led);
782                mStateMessage2.setVisibility(View.VISIBLE);
783                mStateMessage2.setText(res.getString(R.string.recording));
784
785                mExitButtons.setVisibility(View.INVISIBLE);
786                mVUMeter.setVisibility(View.VISIBLE);
787
788                mStateProgressBar.setVisibility(View.INVISIBLE);
789
790                setTitle(res.getString(R.string.record_your_message));
791
792                break;
793
794            case Recorder.PLAYING_STATE:
795                mRecordButton.setEnabled(true);
796                mRecordButton.setFocusable(true);
797                mPlayButton.setEnabled(false);
798                mPlayButton.setFocusable(false);
799                mStopButton.setEnabled(true);
800                mStopButton.setFocusable(true);
801
802                mStateMessage1.setVisibility(View.INVISIBLE);
803                mStateLED.setVisibility(View.INVISIBLE);
804                mStateMessage2.setVisibility(View.INVISIBLE);
805
806                mExitButtons.setVisibility(View.VISIBLE);
807                mVUMeter.setVisibility(View.INVISIBLE);
808
809                mStateProgressBar.setVisibility(View.VISIBLE);
810
811                setTitle(res.getString(R.string.review_message));
812
813                break;
814        }
815
816        updateTimerView();
817        mVUMeter.invalidate();
818    }
819
820    /*
821     * Called when Recorder changed it's state.
822     */
823    public void onStateChanged(int state) {
824        if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) {
825            mSampleInterrupted = false;
826            mErrorUiMessage = null;
827        }
828
829        if (state == Recorder.RECORDING_STATE) {
830            mWakeLock.acquire(); // we don't want to go to sleep while recording
831        } else {
832            if (mWakeLock.isHeld())
833                mWakeLock.release();
834        }
835
836        updateUi();
837    }
838
839    /*
840     * Called when MediaPlayer encounters an error.
841     */
842    public void onError(int error) {
843        Resources res = getResources();
844
845        String message = null;
846        switch (error) {
847            case Recorder.SDCARD_ACCESS_ERROR:
848                message = res.getString(R.string.error_sdcard_access);
849                break;
850            case Recorder.IN_CALL_RECORD_ERROR:
851                // TODO: update error message to reflect that the recording could not be
852                //       performed during a call.
853            case Recorder.INTERNAL_ERROR:
854                message = res.getString(R.string.error_app_internal);
855                break;
856        }
857        if (message != null) {
858            new AlertDialog.Builder(this)
859                .setTitle(R.string.app_name)
860                .setMessage(message)
861                .setPositiveButton(R.string.button_ok, null)
862                .setCancelable(false)
863                .show();
864        }
865    }
866}
867