• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RemainingTimeCalculator()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 
setFileSizeLimit(File file, long maxBytes)101     public void setFileSizeLimit(File file, long maxBytes) {
102         mRecordingFile = file;
103         mMaxBytes = maxBytes;
104     }
105 
106     /**
107      * Resets the interpolation.
108      */
reset()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      */
timeRemaining()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      */
setBitRate(int bitRate)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
onCreate(Bundle icycle)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
onConfigurationChanged(Configuration newConfig)297     public void onConfigurationChanged(Configuration newConfig) {
298         super.onConfigurationChanged(newConfig);
299 
300         setContentView(R.layout.main);
301         initResourceRefs();
302         updateUi();
303     }
304 
305     @Override
onSaveInstanceState(Bundle outState)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      */
initResourceRefs()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      */
stopAudioPlayback()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      */
onClick(View button)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
onKeyDown(int keyCode, KeyEvent event)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
onStop()448     public void onStop() {
449         mRecorder.stop();
450         super.onStop();
451     }
452 
453     @Override
onPause()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      */
saveSample()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
onDestroy()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      */
registerExternalStorageListener()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      */
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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      */
addToPlaylist(ContentResolver resolver, int audioId, long playlistId)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      */
getPlaylistId(Resources res)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      */
createPlaylist(Resources res, ContentResolver resolver)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      */
addToMediaDB(File file)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      */
updateTimerView()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      */
updateTimeRemaining()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      */
updateUi()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      */
onStateChanged(int state)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      */
onError(int error)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