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