Home | History | Annotate | Download | only in soundrecorder
      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 
     17 package com.android.soundrecorder;
     18 
     19 import java.io.File;
     20 import java.text.SimpleDateFormat;
     21 import java.util.Date;
     22 
     23 import android.app.Activity;
     24 import android.app.AlertDialog;
     25 import android.content.ContentResolver;
     26 import android.content.ContentValues;
     27 import android.content.Intent;
     28 import android.content.Context;
     29 import android.content.IntentFilter;
     30 import android.content.BroadcastReceiver;
     31 import android.content.res.Configuration;
     32 import android.content.res.Resources;
     33 import android.database.Cursor;
     34 import android.media.AudioManager;
     35 import android.media.MediaRecorder;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.os.Environment;
     39 import android.os.Handler;
     40 import android.os.PowerManager;
     41 import android.os.StatFs;
     42 import android.os.PowerManager.WakeLock;
     43 import android.provider.MediaStore;
     44 import android.util.Log;
     45 import android.view.KeyEvent;
     46 import android.view.View;
     47 import android.widget.Button;
     48 import android.widget.ImageButton;
     49 import android.widget.ImageView;
     50 import android.widget.LinearLayout;
     51 import android.widget.ProgressBar;
     52 import 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 
     62 class 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 
    195 public 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                                          .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
    476     }
    477 
    478     /*
    479      * Called on destroy to unregister the SD card mount event receiver.
    480      */
    481     @Override
    482     public void onDestroy() {
    483         if (mSDCardMountEventReceiver != null) {
    484             unregisterReceiver(mSDCardMountEventReceiver);
    485             mSDCardMountEventReceiver = null;
    486         }
    487         super.onDestroy();
    488     }
    489 
    490     /*
    491      * Registers an intent to listen for ACTION_MEDIA_EJECT/ACTION_MEDIA_MOUNTED
    492      * notifications.
    493      */
    494     private void registerExternalStorageListener() {
    495         if (mSDCardMountEventReceiver == null) {
    496             mSDCardMountEventReceiver = new BroadcastReceiver() {
    497                 @Override
    498                 public void onReceive(Context context, Intent intent) {
    499                     String action = intent.getAction();
    500                     if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
    501                         mRecorder.delete();
    502                     } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
    503                         mSampleInterrupted = false;
    504                         updateUi();
    505                     }
    506                 }
    507             };
    508             IntentFilter iFilter = new IntentFilter();
    509             iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
    510             iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
    511             iFilter.addDataScheme("file");
    512             registerReceiver(mSDCardMountEventReceiver, iFilter);
    513         }
    514     }
    515 
    516     /*
    517      * A simple utility to do a query into the databases.
    518      */
    519     private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    520         try {
    521             ContentResolver resolver = getContentResolver();
    522             if (resolver == null) {
    523                 return null;
    524             }
    525             return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
    526          } catch (UnsupportedOperationException ex) {
    527             return null;
    528         }
    529     }
    530 
    531     /*
    532      * Add the given audioId to the playlist with the given playlistId; and maintain the
    533      * play_order in the playlist.
    534      */
    535     private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) {
    536         String[] cols = new String[] {
    537                 "count(*)"
    538         };
    539         Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
    540         Cursor cur = resolver.query(uri, cols, null, null, null);
    541         cur.moveToFirst();
    542         final int base = cur.getInt(0);
    543         cur.close();
    544         ContentValues values = new ContentValues();
    545         values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId));
    546         values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
    547         resolver.insert(uri, values);
    548     }
    549 
    550     /*
    551      * Obtain the id for the default play list from the audio_playlists table.
    552      */
    553     private int getPlaylistId(Resources res) {
    554         Uri uri = MediaStore.Audio.Playlists.getContentUri("external");
    555         final String[] ids = new String[] { MediaStore.Audio.Playlists._ID };
    556         final String where = MediaStore.Audio.Playlists.NAME + "=?";
    557         final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) };
    558         Cursor cursor = query(uri, ids, where, args, null);
    559         if (cursor == null) {
    560             Log.v(TAG, "query returns null");
    561         }
    562         int id = -1;
    563         if (cursor != null) {
    564             cursor.moveToFirst();
    565             if (!cursor.isAfterLast()) {
    566                 id = cursor.getInt(0);
    567             }
    568         }
    569         cursor.close();
    570         return id;
    571     }
    572 
    573     /*
    574      * Create a playlist with the given default playlist name, if no such playlist exists.
    575      */
    576     private Uri createPlaylist(Resources res, ContentResolver resolver) {
    577         ContentValues cv = new ContentValues();
    578         cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name));
    579         Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
    580         if (uri == null) {
    581             new AlertDialog.Builder(this)
    582                 .setTitle(R.string.app_name)
    583                 .setMessage(R.string.error_mediadb_new_record)
    584                 .setPositiveButton(R.string.button_ok, null)
    585                 .setCancelable(false)
    586                 .show();
    587         }
    588         return uri;
    589     }
    590 
    591     /*
    592      * Adds file and returns content uri.
    593      */
    594     private Uri addToMediaDB(File file) {
    595         Resources res = getResources();
    596         ContentValues cv = new ContentValues();
    597         long current = System.currentTimeMillis();
    598         long modDate = file.lastModified();
    599         Date date = new Date(current);
    600         SimpleDateFormat formatter = new SimpleDateFormat(
    601                 res.getString(R.string.audio_db_title_format));
    602         String title = formatter.format(date);
    603         long sampleLengthMillis = mRecorder.sampleLength() * 1000L;
    604 
    605         // Lets label the recorded audio file as NON-MUSIC so that the file
    606         // won't be displayed automatically, except for in the playlist.
    607         cv.put(MediaStore.Audio.Media.IS_MUSIC, "0");
    608 
    609         cv.put(MediaStore.Audio.Media.TITLE, title);
    610         cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
    611         cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000));
    612         cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000));
    613         cv.put(MediaStore.Audio.Media.DURATION, sampleLengthMillis);
    614         cv.put(MediaStore.Audio.Media.MIME_TYPE, mRequestedType);
    615         cv.put(MediaStore.Audio.Media.ARTIST,
    616                 res.getString(R.string.audio_db_artist_name));
    617         cv.put(MediaStore.Audio.Media.ALBUM,
    618                 res.getString(R.string.audio_db_album_name));
    619         Log.d(TAG, "Inserting audio record: " + cv.toString());
    620         ContentResolver resolver = getContentResolver();
    621         Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    622         Log.d(TAG, "ContentURI: " + base);
    623         Uri result = resolver.insert(base, cv);
    624         if (result == null) {
    625             new AlertDialog.Builder(this)
    626                 .setTitle(R.string.app_name)
    627                 .setMessage(R.string.error_mediadb_new_record)
    628                 .setPositiveButton(R.string.button_ok, null)
    629                 .setCancelable(false)
    630                 .show();
    631             return null;
    632         }
    633         if (getPlaylistId(res) == -1) {
    634             createPlaylist(res, resolver);
    635         }
    636         int audioId = Integer.valueOf(result.getLastPathSegment());
    637         addToPlaylist(resolver, audioId, getPlaylistId(res));
    638 
    639         // Notify those applications such as Music listening to the
    640         // scanner events that a recorded audio file just created.
    641         sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
    642         return result;
    643     }
    644 
    645     /**
    646      * Update the big MM:SS timer. If we are in playback, also update the
    647      * progress bar.
    648      */
    649     private void updateTimerView() {
    650         Resources res = getResources();
    651         int state = mRecorder.state();
    652 
    653         boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE;
    654 
    655         long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength();
    656         String timeStr = String.format(mTimerFormat, time/60, time%60);
    657         mTimerView.setText(timeStr);
    658 
    659         if (state == Recorder.PLAYING_STATE) {
    660             mStateProgressBar.setProgress((int)(100*time/mRecorder.sampleLength()));
    661         } else if (state == Recorder.RECORDING_STATE) {
    662             updateTimeRemaining();
    663         }
    664 
    665         if (ongoing)
    666             mHandler.postDelayed(mUpdateTimer, 1000);
    667     }
    668 
    669     /*
    670      * Called when we're in recording state. Find out how much longer we can
    671      * go on recording. If it's under 5 minutes, we display a count-down in
    672      * the UI. If we've run out of time, stop the recording.
    673      */
    674     private void updateTimeRemaining() {
    675         long t = mRemainingTimeCalculator.timeRemaining();
    676 
    677         if (t <= 0) {
    678             mSampleInterrupted = true;
    679 
    680             int limit = mRemainingTimeCalculator.currentLowerLimit();
    681             switch (limit) {
    682                 case RemainingTimeCalculator.DISK_SPACE_LIMIT:
    683                     mErrorUiMessage
    684                         = getResources().getString(R.string.storage_is_full);
    685                     break;
    686                 case RemainingTimeCalculator.FILE_SIZE_LIMIT:
    687                     mErrorUiMessage
    688                         = getResources().getString(R.string.max_length_reached);
    689                     break;
    690                 default:
    691                     mErrorUiMessage = null;
    692                     break;
    693             }
    694 
    695             mRecorder.stop();
    696             return;
    697         }
    698 
    699         Resources res = getResources();
    700         String timeStr = "";
    701 
    702         if (t < 60)
    703             timeStr = String.format(res.getString(R.string.sec_available), t);
    704         else if (t < 540)
    705             timeStr = String.format(res.getString(R.string.min_available), t/60 + 1);
    706 
    707         mStateMessage1.setText(timeStr);
    708     }
    709 
    710     /**
    711      * Shows/hides the appropriate child views for the new state.
    712      */
    713     private void updateUi() {
    714         Resources res = getResources();
    715 
    716         switch (mRecorder.state()) {
    717             case Recorder.IDLE_STATE:
    718                 if (mRecorder.sampleLength() == 0) {
    719                     mRecordButton.setEnabled(true);
    720                     mRecordButton.setFocusable(true);
    721                     mPlayButton.setEnabled(false);
    722                     mPlayButton.setFocusable(false);
    723                     mStopButton.setEnabled(false);
    724                     mStopButton.setFocusable(false);
    725                     mRecordButton.requestFocus();
    726 
    727                     mStateMessage1.setVisibility(View.INVISIBLE);
    728                     mStateLED.setVisibility(View.INVISIBLE);
    729                     mStateMessage2.setVisibility(View.INVISIBLE);
    730 
    731                     mExitButtons.setVisibility(View.INVISIBLE);
    732                     mVUMeter.setVisibility(View.VISIBLE);
    733 
    734                     mStateProgressBar.setVisibility(View.INVISIBLE);
    735 
    736                     setTitle(res.getString(R.string.record_your_message));
    737                 } else {
    738                     mRecordButton.setEnabled(true);
    739                     mRecordButton.setFocusable(true);
    740                     mPlayButton.setEnabled(true);
    741                     mPlayButton.setFocusable(true);
    742                     mStopButton.setEnabled(false);
    743                     mStopButton.setFocusable(false);
    744 
    745                     mStateMessage1.setVisibility(View.INVISIBLE);
    746                     mStateLED.setVisibility(View.INVISIBLE);
    747                     mStateMessage2.setVisibility(View.INVISIBLE);
    748 
    749                     mExitButtons.setVisibility(View.VISIBLE);
    750                     mVUMeter.setVisibility(View.INVISIBLE);
    751 
    752                     mStateProgressBar.setVisibility(View.INVISIBLE);
    753 
    754                     setTitle(res.getString(R.string.message_recorded));
    755                 }
    756 
    757                 if (mSampleInterrupted) {
    758                     mStateMessage2.setVisibility(View.VISIBLE);
    759                     mStateMessage2.setText(res.getString(R.string.recording_stopped));
    760                     mStateLED.setVisibility(View.INVISIBLE);
    761                 }
    762 
    763                 if (mErrorUiMessage != null) {
    764                     mStateMessage1.setText(mErrorUiMessage);
    765                     mStateMessage1.setVisibility(View.VISIBLE);
    766                 }
    767 
    768                 break;
    769             case Recorder.RECORDING_STATE:
    770                 mRecordButton.setEnabled(false);
    771                 mRecordButton.setFocusable(false);
    772                 mPlayButton.setEnabled(false);
    773                 mPlayButton.setFocusable(false);
    774                 mStopButton.setEnabled(true);
    775                 mStopButton.setFocusable(true);
    776 
    777                 mStateMessage1.setVisibility(View.VISIBLE);
    778                 mStateLED.setVisibility(View.VISIBLE);
    779                 mStateLED.setImageResource(R.drawable.recording_led);
    780                 mStateMessage2.setVisibility(View.VISIBLE);
    781                 mStateMessage2.setText(res.getString(R.string.recording));
    782 
    783                 mExitButtons.setVisibility(View.INVISIBLE);
    784                 mVUMeter.setVisibility(View.VISIBLE);
    785 
    786                 mStateProgressBar.setVisibility(View.INVISIBLE);
    787 
    788                 setTitle(res.getString(R.string.record_your_message));
    789 
    790                 break;
    791 
    792             case Recorder.PLAYING_STATE:
    793                 mRecordButton.setEnabled(true);
    794                 mRecordButton.setFocusable(true);
    795                 mPlayButton.setEnabled(false);
    796                 mPlayButton.setFocusable(false);
    797                 mStopButton.setEnabled(true);
    798                 mStopButton.setFocusable(true);
    799 
    800                 mStateMessage1.setVisibility(View.INVISIBLE);
    801                 mStateLED.setVisibility(View.INVISIBLE);
    802                 mStateMessage2.setVisibility(View.INVISIBLE);
    803 
    804                 mExitButtons.setVisibility(View.VISIBLE);
    805                 mVUMeter.setVisibility(View.INVISIBLE);
    806 
    807                 mStateProgressBar.setVisibility(View.VISIBLE);
    808 
    809                 setTitle(res.getString(R.string.review_message));
    810 
    811                 break;
    812         }
    813 
    814         updateTimerView();
    815         mVUMeter.invalidate();
    816     }
    817 
    818     /*
    819      * Called when Recorder changed it's state.
    820      */
    821     public void onStateChanged(int state) {
    822         if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) {
    823             mSampleInterrupted = false;
    824             mErrorUiMessage = null;
    825             mWakeLock.acquire(); // we don't want to go to sleep while recording or playing
    826         } else {
    827             if (mWakeLock.isHeld())
    828                 mWakeLock.release();
    829         }
    830 
    831         updateUi();
    832     }
    833 
    834     /*
    835      * Called when MediaPlayer encounters an error.
    836      */
    837     public void onError(int error) {
    838         Resources res = getResources();
    839 
    840         String message = null;
    841         switch (error) {
    842             case Recorder.SDCARD_ACCESS_ERROR:
    843                 message = res.getString(R.string.error_sdcard_access);
    844                 break;
    845             case Recorder.IN_CALL_RECORD_ERROR:
    846                 // TODO: update error message to reflect that the recording could not be
    847                 //       performed during a call.
    848             case Recorder.INTERNAL_ERROR:
    849                 message = res.getString(R.string.error_app_internal);
    850                 break;
    851         }
    852         if (message != null) {
    853             new AlertDialog.Builder(this)
    854                 .setTitle(R.string.app_name)
    855                 .setMessage(message)
    856                 .setPositiveButton(R.string.button_ok, null)
    857                 .setCancelable(false)
    858                 .show();
    859         }
    860     }
    861 }
    862