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