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 17package com.android.soundrecorder; 18 19import java.io.File; 20import java.text.SimpleDateFormat; 21import java.util.Date; 22 23import android.app.Activity; 24import android.app.AlertDialog; 25import android.content.ContentResolver; 26import android.content.ContentValues; 27import android.content.Intent; 28import android.content.Context; 29import android.content.IntentFilter; 30import android.content.BroadcastReceiver; 31import android.content.res.Configuration; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.media.AudioManager; 35import android.media.MediaRecorder; 36import android.net.Uri; 37import android.os.Bundle; 38import android.os.Environment; 39import android.os.Handler; 40import android.os.PowerManager; 41import android.os.StatFs; 42import android.os.PowerManager.WakeLock; 43import android.provider.MediaStore; 44import android.util.Log; 45import android.view.KeyEvent; 46import android.view.View; 47import android.widget.Button; 48import android.widget.ImageButton; 49import android.widget.ImageView; 50import android.widget.LinearLayout; 51import android.widget.ProgressBar; 52import 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 62class 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 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 102 public void setFileSizeLimit(File file, long maxBytes) { 103 mRecordingFile = file; 104 mMaxBytes = maxBytes; 105 } 106 107 /** 108 * Resets the interpolation. 109 */ 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 */ 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 */ 190 public void setBitRate(int bitRate) { 191 mBytesPerSecond = bitRate/8; 192 } 193} 194 195public 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 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 298 public void onConfigurationChanged(Configuration newConfig) { 299 super.onConfigurationChanged(newConfig); 300 301 setContentView(R.layout.main); 302 initResourceRefs(); 303 updateUi(); 304 } 305 306 @Override 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 */ 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 */ 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 */ 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 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 445 public void onStop() { 446 mRecorder.stop(); 447 super.onStop(); 448 } 449 450 @Override 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 */ 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 } 476 477 /* 478 * Called on destroy to unregister the SD card mount event receiver. 479 */ 480 @Override 481 public void onDestroy() { 482 if (mSDCardMountEventReceiver != null) { 483 unregisterReceiver(mSDCardMountEventReceiver); 484 mSDCardMountEventReceiver = null; 485 } 486 super.onDestroy(); 487 } 488 489 /* 490 * Registers an intent to listen for ACTION_MEDIA_EJECT/ACTION_MEDIA_MOUNTED 491 * notifications. 492 */ 493 private void registerExternalStorageListener() { 494 if (mSDCardMountEventReceiver == null) { 495 mSDCardMountEventReceiver = new BroadcastReceiver() { 496 @Override 497 public void onReceive(Context context, Intent intent) { 498 String action = intent.getAction(); 499 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 500 mRecorder.delete(); 501 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 502 mSampleInterrupted = false; 503 updateUi(); 504 } 505 } 506 }; 507 IntentFilter iFilter = new IntentFilter(); 508 iFilter.addAction(Intent.ACTION_MEDIA_EJECT); 509 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 510 iFilter.addDataScheme("file"); 511 registerReceiver(mSDCardMountEventReceiver, iFilter); 512 } 513 } 514 515 /* 516 * A simple utility to do a query into the databases. 517 */ 518 private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 519 try { 520 ContentResolver resolver = getContentResolver(); 521 if (resolver == null) { 522 return null; 523 } 524 return resolver.query(uri, projection, selection, selectionArgs, sortOrder); 525 } catch (UnsupportedOperationException ex) { 526 return null; 527 } 528 } 529 530 /* 531 * Add the given audioId to the playlist with the given playlistId; and maintain the 532 * play_order in the playlist. 533 */ 534 private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) { 535 String[] cols = new String[] { 536 "count(*)" 537 }; 538 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); 539 Cursor cur = resolver.query(uri, cols, null, null, null); 540 cur.moveToFirst(); 541 final int base = cur.getInt(0); 542 cur.close(); 543 ContentValues values = new ContentValues(); 544 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId)); 545 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 546 resolver.insert(uri, values); 547 } 548 549 /* 550 * Obtain the id for the default play list from the audio_playlists table. 551 */ 552 private int getPlaylistId(Resources res) { 553 Uri uri = MediaStore.Audio.Playlists.getContentUri("external"); 554 final String[] ids = new String[] { MediaStore.Audio.Playlists._ID }; 555 final String where = MediaStore.Audio.Playlists.NAME + "=?"; 556 final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) }; 557 Cursor cursor = query(uri, ids, where, args, null); 558 if (cursor == null) { 559 Log.v(TAG, "query returns null"); 560 } 561 int id = -1; 562 if (cursor != null) { 563 cursor.moveToFirst(); 564 if (!cursor.isAfterLast()) { 565 id = cursor.getInt(0); 566 } 567 } 568 cursor.close(); 569 return id; 570 } 571 572 /* 573 * Create a playlist with the given default playlist name, if no such playlist exists. 574 */ 575 private Uri createPlaylist(Resources res, ContentResolver resolver) { 576 ContentValues cv = new ContentValues(); 577 cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name)); 578 Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv); 579 if (uri == null) { 580 new AlertDialog.Builder(this) 581 .setTitle(R.string.app_name) 582 .setMessage(R.string.error_mediadb_new_record) 583 .setPositiveButton(R.string.button_ok, null) 584 .setCancelable(false) 585 .show(); 586 } 587 return uri; 588 } 589 590 /* 591 * Adds file and returns content uri. 592 */ 593 private Uri addToMediaDB(File file) { 594 Resources res = getResources(); 595 ContentValues cv = new ContentValues(); 596 long current = System.currentTimeMillis(); 597 long modDate = file.lastModified(); 598 Date date = new Date(current); 599 SimpleDateFormat formatter = new SimpleDateFormat( 600 res.getString(R.string.audio_db_title_format)); 601 String title = formatter.format(date); 602 long sampleLengthMillis = mRecorder.sampleLength() * 1000L; 603 604 // Lets label the recorded audio file as NON-MUSIC so that the file 605 // won't be displayed automatically, except for in the playlist. 606 cv.put(MediaStore.Audio.Media.IS_MUSIC, "0"); 607 608 cv.put(MediaStore.Audio.Media.TITLE, title); 609 cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath()); 610 cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000)); 611 cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000)); 612 cv.put(MediaStore.Audio.Media.DURATION, sampleLengthMillis); 613 cv.put(MediaStore.Audio.Media.MIME_TYPE, mRequestedType); 614 cv.put(MediaStore.Audio.Media.ARTIST, 615 res.getString(R.string.audio_db_artist_name)); 616 cv.put(MediaStore.Audio.Media.ALBUM, 617 res.getString(R.string.audio_db_album_name)); 618 Log.d(TAG, "Inserting audio record: " + cv.toString()); 619 ContentResolver resolver = getContentResolver(); 620 Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 621 Log.d(TAG, "ContentURI: " + base); 622 Uri result = resolver.insert(base, cv); 623 if (result == null) { 624 new AlertDialog.Builder(this) 625 .setTitle(R.string.app_name) 626 .setMessage(R.string.error_mediadb_new_record) 627 .setPositiveButton(R.string.button_ok, null) 628 .setCancelable(false) 629 .show(); 630 return null; 631 } 632 if (getPlaylistId(res) == -1) { 633 createPlaylist(res, resolver); 634 } 635 int audioId = Integer.valueOf(result.getLastPathSegment()); 636 addToPlaylist(resolver, audioId, getPlaylistId(res)); 637 638 // Notify those applications such as Music listening to the 639 // scanner events that a recorded audio file just created. 640 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); 641 return result; 642 } 643 644 /** 645 * Update the big MM:SS timer. If we are in playback, also update the 646 * progress bar. 647 */ 648 private void updateTimerView() { 649 Resources res = getResources(); 650 int state = mRecorder.state(); 651 652 boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE; 653 654 long time = ongoing ? mRecorder.progress() : mRecorder.sampleLength(); 655 String timeStr = String.format(mTimerFormat, time/60, time%60); 656 mTimerView.setText(timeStr); 657 658 if (state == Recorder.PLAYING_STATE) { 659 mStateProgressBar.setProgress((int)(100*time/mRecorder.sampleLength())); 660 } else if (state == Recorder.RECORDING_STATE) { 661 updateTimeRemaining(); 662 } 663 664 if (ongoing) 665 mHandler.postDelayed(mUpdateTimer, 1000); 666 } 667 668 /* 669 * Called when we're in recording state. Find out how much longer we can 670 * go on recording. If it's under 5 minutes, we display a count-down in 671 * the UI. If we've run out of time, stop the recording. 672 */ 673 private void updateTimeRemaining() { 674 long t = mRemainingTimeCalculator.timeRemaining(); 675 676 if (t <= 0) { 677 mSampleInterrupted = true; 678 679 int limit = mRemainingTimeCalculator.currentLowerLimit(); 680 switch (limit) { 681 case RemainingTimeCalculator.DISK_SPACE_LIMIT: 682 mErrorUiMessage 683 = getResources().getString(R.string.storage_is_full); 684 break; 685 case RemainingTimeCalculator.FILE_SIZE_LIMIT: 686 mErrorUiMessage 687 = getResources().getString(R.string.max_length_reached); 688 break; 689 default: 690 mErrorUiMessage = null; 691 break; 692 } 693 694 mRecorder.stop(); 695 return; 696 } 697 698 Resources res = getResources(); 699 String timeStr = ""; 700 701 if (t < 60) 702 timeStr = String.format(res.getString(R.string.sec_available), t); 703 else if (t < 540) 704 timeStr = String.format(res.getString(R.string.min_available), t/60 + 1); 705 706 mStateMessage1.setText(timeStr); 707 } 708 709 /** 710 * Shows/hides the appropriate child views for the new state. 711 */ 712 private void updateUi() { 713 Resources res = getResources(); 714 715 switch (mRecorder.state()) { 716 case Recorder.IDLE_STATE: 717 if (mRecorder.sampleLength() == 0) { 718 mRecordButton.setEnabled(true); 719 mRecordButton.setFocusable(true); 720 mPlayButton.setEnabled(false); 721 mPlayButton.setFocusable(false); 722 mStopButton.setEnabled(false); 723 mStopButton.setFocusable(false); 724 mRecordButton.requestFocus(); 725 726 mStateMessage1.setVisibility(View.INVISIBLE); 727 mStateLED.setVisibility(View.INVISIBLE); 728 mStateMessage2.setVisibility(View.INVISIBLE); 729 730 mExitButtons.setVisibility(View.INVISIBLE); 731 mVUMeter.setVisibility(View.VISIBLE); 732 733 mStateProgressBar.setVisibility(View.INVISIBLE); 734 735 setTitle(res.getString(R.string.record_your_message)); 736 } else { 737 mRecordButton.setEnabled(true); 738 mRecordButton.setFocusable(true); 739 mPlayButton.setEnabled(true); 740 mPlayButton.setFocusable(true); 741 mStopButton.setEnabled(false); 742 mStopButton.setFocusable(false); 743 744 mStateMessage1.setVisibility(View.INVISIBLE); 745 mStateLED.setVisibility(View.INVISIBLE); 746 mStateMessage2.setVisibility(View.INVISIBLE); 747 748 mExitButtons.setVisibility(View.VISIBLE); 749 mVUMeter.setVisibility(View.INVISIBLE); 750 751 mStateProgressBar.setVisibility(View.INVISIBLE); 752 753 setTitle(res.getString(R.string.message_recorded)); 754 } 755 756 if (mSampleInterrupted) { 757 mStateMessage2.setVisibility(View.VISIBLE); 758 mStateMessage2.setText(res.getString(R.string.recording_stopped)); 759 mStateLED.setVisibility(View.INVISIBLE); 760 } 761 762 if (mErrorUiMessage != null) { 763 mStateMessage1.setText(mErrorUiMessage); 764 mStateMessage1.setVisibility(View.VISIBLE); 765 } 766 767 break; 768 case Recorder.RECORDING_STATE: 769 mRecordButton.setEnabled(false); 770 mRecordButton.setFocusable(false); 771 mPlayButton.setEnabled(false); 772 mPlayButton.setFocusable(false); 773 mStopButton.setEnabled(true); 774 mStopButton.setFocusable(true); 775 776 mStateMessage1.setVisibility(View.VISIBLE); 777 mStateLED.setVisibility(View.VISIBLE); 778 mStateLED.setImageResource(R.drawable.recording_led); 779 mStateMessage2.setVisibility(View.VISIBLE); 780 mStateMessage2.setText(res.getString(R.string.recording)); 781 782 mExitButtons.setVisibility(View.INVISIBLE); 783 mVUMeter.setVisibility(View.VISIBLE); 784 785 mStateProgressBar.setVisibility(View.INVISIBLE); 786 787 setTitle(res.getString(R.string.record_your_message)); 788 789 break; 790 791 case Recorder.PLAYING_STATE: 792 mRecordButton.setEnabled(true); 793 mRecordButton.setFocusable(true); 794 mPlayButton.setEnabled(false); 795 mPlayButton.setFocusable(false); 796 mStopButton.setEnabled(true); 797 mStopButton.setFocusable(true); 798 799 mStateMessage1.setVisibility(View.INVISIBLE); 800 mStateLED.setVisibility(View.INVISIBLE); 801 mStateMessage2.setVisibility(View.INVISIBLE); 802 803 mExitButtons.setVisibility(View.VISIBLE); 804 mVUMeter.setVisibility(View.INVISIBLE); 805 806 mStateProgressBar.setVisibility(View.VISIBLE); 807 808 setTitle(res.getString(R.string.review_message)); 809 810 break; 811 } 812 813 updateTimerView(); 814 mVUMeter.invalidate(); 815 } 816 817 /* 818 * Called when Recorder changed it's state. 819 */ 820 public void onStateChanged(int state) { 821 if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) { 822 mSampleInterrupted = false; 823 mErrorUiMessage = null; 824 mWakeLock.acquire(); // we don't want to go to sleep while recording or playing 825 } else { 826 if (mWakeLock.isHeld()) 827 mWakeLock.release(); 828 } 829 830 updateUi(); 831 } 832 833 /* 834 * Called when MediaPlayer encounters an error. 835 */ 836 public void onError(int error) { 837 Resources res = getResources(); 838 839 String message = null; 840 switch (error) { 841 case Recorder.SDCARD_ACCESS_ERROR: 842 message = res.getString(R.string.error_sdcard_access); 843 break; 844 case Recorder.IN_CALL_RECORD_ERROR: 845 // TODO: update error message to reflect that the recording could not be 846 // performed during a call. 847 case Recorder.INTERNAL_ERROR: 848 message = res.getString(R.string.error_app_internal); 849 break; 850 } 851 if (message != null) { 852 new AlertDialog.Builder(this) 853 .setTitle(R.string.app_name) 854 .setMessage(message) 855 .setPositiveButton(R.string.button_ok, null) 856 .setCancelable(false) 857 .show(); 858 } 859 } 860} 861