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