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