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