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