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.MediaRecorder;
     35 import android.net.Uri;
     36 import android.os.Bundle;
     37 import android.os.Environment;
     38 import android.os.Handler;
     39 import android.os.PowerManager;
     40 import android.os.StatFs;
     41 import android.os.PowerManager.WakeLock;
     42 import android.provider.MediaStore;
     43 import android.util.Log;
     44 import android.view.KeyEvent;
     45 import android.view.View;
     46 import android.widget.Button;
     47 import android.widget.ImageButton;
     48 import android.widget.ImageView;
     49 import android.widget.LinearLayout;
     50 import android.widget.ProgressBar;
     51 import 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 
     61 class 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 
    194 public 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             mWakeLock.acquire(); // we don't want to go to sleep while recording or playing
    828         } else {
    829             if (mWakeLock.isHeld())
    830                 mWakeLock.release();
    831         }
    832 
    833         updateUi();
    834     }
    835 
    836     /*
    837      * Called when MediaPlayer encounters an error.
    838      */
    839     public void onError(int error) {
    840         Resources res = getResources();
    841 
    842         String message = null;
    843         switch (error) {
    844             case Recorder.SDCARD_ACCESS_ERROR:
    845                 message = res.getString(R.string.error_sdcard_access);
    846                 break;
    847             case Recorder.IN_CALL_RECORD_ERROR:
    848                 // TODO: update error message to reflect that the recording could not be
    849                 //       performed during a call.
    850             case Recorder.INTERNAL_ERROR:
    851                 message = res.getString(R.string.error_app_internal);
    852                 break;
    853         }
    854         if (message != null) {
    855             new AlertDialog.Builder(this)
    856                 .setTitle(R.string.app_name)
    857                 .setMessage(message)
    858                 .setPositiveButton(R.string.button_ok, null)
    859                 .setCancelable(false)
    860                 .show();
    861         }
    862     }
    863 }
    864