1 /* 2 * Copyright (C) 2014 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.fmradio; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.media.MediaPlayer; 25 import android.media.MediaRecorder; 26 import android.media.MediaScannerConnection; 27 import android.net.Uri; 28 import android.os.Environment; 29 import android.os.SystemClock; 30 import android.provider.MediaStore; 31 import android.text.format.DateFormat; 32 import android.util.Log; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.text.SimpleDateFormat; 37 import java.util.Date; 38 import java.util.Locale; 39 40 /** 41 * This class provider interface to recording, stop recording, save recording 42 * file, play recording file 43 */ 44 public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener { 45 private static final String TAG = "FmRecorder"; 46 // file prefix 47 public static final String RECORDING_FILE_PREFIX = "FM"; 48 // file extension 49 public static final String RECORDING_FILE_EXTENSION = ".3gpp"; 50 // recording file folder 51 public static final String FM_RECORD_FOLDER = "FM Recording"; 52 private static final String RECORDING_FILE_TYPE = "audio/3gpp"; 53 private static final String RECORDING_FILE_SOURCE = "FM Recordings"; 54 // error type no sdcard 55 public static final int ERROR_SDCARD_NOT_PRESENT = 0; 56 // error type sdcard not have enough space 57 public static final int ERROR_SDCARD_INSUFFICIENT_SPACE = 1; 58 // error type can't write sdcard 59 public static final int ERROR_SDCARD_WRITE_FAILED = 2; 60 // error type recorder internal error occur 61 public static final int ERROR_RECORDER_INTERNAL = 3; 62 63 // FM Recorder state not recording and not playing 64 public static final int STATE_IDLE = 5; 65 // FM Recorder state recording 66 public static final int STATE_RECORDING = 6; 67 // FM Recorder state playing 68 public static final int STATE_PLAYBACK = 7; 69 // FM Recorder state invalid, need to check 70 public static final int STATE_INVALID = -1; 71 72 // use to record current FM recorder state 73 public int mInternalState = STATE_IDLE; 74 // the recording time after start recording 75 private long mRecordTime = 0; 76 // record start time 77 private long mRecordStartTime = 0; 78 // current record file 79 private File mRecordFile = null; 80 // record current record file is saved by user 81 private boolean mIsRecordingFileSaved = false; 82 // listener use for notify service the record state or error state 83 private OnRecorderStateChangedListener mStateListener = null; 84 // recorder use for record file 85 private MediaRecorder mRecorder = null; 86 87 /** 88 * Start recording the voice of FM, also check the pre-conditions, if not 89 * meet, will return an error message to the caller. if can start recording 90 * success, will set FM record state to recording and notify to the caller 91 */ startRecording(Context context)92 public void startRecording(Context context) { 93 mRecordTime = 0; 94 95 // Check external storage 96 if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { 97 Log.e(TAG, "startRecording, no external storage available"); 98 setError(ERROR_SDCARD_NOT_PRESENT); 99 return; 100 } 101 102 String recordingSdcard = FmUtils.getDefaultStoragePath(); 103 // check whether have sufficient storage space, if not will notify 104 // caller error message 105 if (!FmUtils.hasEnoughSpace(recordingSdcard)) { 106 setError(ERROR_SDCARD_INSUFFICIENT_SPACE); 107 Log.e(TAG, "startRecording, SD card does not have sufficient space!!"); 108 return; 109 } 110 111 // get external storage directory 112 File sdDir = new File(recordingSdcard); 113 File recordingDir = new File(sdDir, FM_RECORD_FOLDER); 114 // exist a file named FM Recording, so can't create FM recording folder 115 if (recordingDir.exists() && !recordingDir.isDirectory()) { 116 Log.e(TAG, "startRecording, a file with name \"FM Recording\" already exists!!"); 117 setError(ERROR_SDCARD_WRITE_FAILED); 118 return; 119 } else if (!recordingDir.exists()) { // try to create recording folder 120 boolean mkdirResult = recordingDir.mkdir(); 121 if (!mkdirResult) { // create recording file failed 122 setError(ERROR_RECORDER_INTERNAL); 123 return; 124 } 125 } 126 // create recording temporary file 127 long curTime = System.currentTimeMillis(); 128 Date date = new Date(curTime); 129 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMddyyyy_HHmmss", 130 Locale.ENGLISH); 131 String time = simpleDateFormat.format(date); 132 StringBuilder stringBuilder = new StringBuilder(); 133 stringBuilder.append(time).append(RECORDING_FILE_EXTENSION); 134 String name = stringBuilder.toString(); 135 mRecordFile = new File(recordingDir, name); 136 try { 137 if (mRecordFile.createNewFile()) { 138 Log.d(TAG, "startRecording, createNewFile success with path " 139 + mRecordFile.getPath()); 140 } 141 } catch (IOException e) { 142 Log.e(TAG, "startRecording, IOException while createTempFile: " + e); 143 e.printStackTrace(); 144 setError(ERROR_SDCARD_WRITE_FAILED); 145 return; 146 } 147 // set record parameter and start recording 148 try { 149 mRecorder = new MediaRecorder(); 150 mRecorder.setOnErrorListener(this); 151 mRecorder.setOnInfoListener(this); 152 mRecorder.setAudioSource(MediaRecorder.AudioSource.FM_TUNER); 153 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 154 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); 155 final int samplingRate = 44100; 156 mRecorder.setAudioSamplingRate(samplingRate); 157 final int bitRate = 128000; 158 mRecorder.setAudioEncodingBitRate(bitRate); 159 final int audiochannels = 2; 160 mRecorder.setAudioChannels(audiochannels); 161 mRecorder.setOutputFile(mRecordFile.getAbsolutePath()); 162 mRecorder.prepare(); 163 mRecordStartTime = SystemClock.elapsedRealtime(); 164 mRecorder.start(); 165 mIsRecordingFileSaved = false; 166 } catch (IllegalStateException e) { 167 Log.e(TAG, "startRecording, IllegalStateException while starting recording!", e); 168 setError(ERROR_RECORDER_INTERNAL); 169 return; 170 } catch (IOException e) { 171 Log.e(TAG, "startRecording, IOException while starting recording!", e); 172 setError(ERROR_RECORDER_INTERNAL); 173 return; 174 } 175 setState(STATE_RECORDING); 176 } 177 178 /** 179 * Stop recording, compute recording time and update FM recorder state 180 */ stopRecording()181 public void stopRecording() { 182 if (STATE_RECORDING != mInternalState) { 183 Log.w(TAG, "stopRecording, called in wrong state!!"); 184 return; 185 } 186 187 mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime; 188 stopRecorder(); 189 setState(STATE_IDLE); 190 } 191 192 /** 193 * Compute the current record time 194 * 195 * @return The current record time 196 */ getRecordTime()197 public long getRecordTime() { 198 if (STATE_RECORDING == mInternalState) { 199 mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime; 200 } 201 return mRecordTime; 202 } 203 204 /** 205 * Get FM recorder current state 206 * 207 * @return FM recorder current state 208 */ getState()209 public int getState() { 210 return mInternalState; 211 } 212 213 /** 214 * Get current record file name 215 * 216 * @return The current record file name 217 */ getRecordFileName()218 public String getRecordFileName() { 219 if (mRecordFile != null) { 220 String fileName = mRecordFile.getName(); 221 int index = fileName.indexOf(RECORDING_FILE_EXTENSION); 222 if (index > 0) { 223 fileName = fileName.substring(0, index); 224 } 225 return fileName; 226 } 227 return null; 228 } 229 230 /** 231 * Save recording file with the given name, and insert it's info to database 232 * 233 * @param context The context 234 * @param newName The name to override default recording name 235 */ saveRecording(Context context, String newName)236 public void saveRecording(Context context, String newName) { 237 if (mRecordFile == null) { 238 Log.e(TAG, "saveRecording, recording file is null!"); 239 return; 240 } 241 242 File newRecordFile = new File(mRecordFile.getParent(), newName + RECORDING_FILE_EXTENSION); 243 boolean succuss = mRecordFile.renameTo(newRecordFile); 244 if (succuss) { 245 mRecordFile = newRecordFile; 246 } 247 mIsRecordingFileSaved = true; 248 // insert recording file info to database 249 addRecordingToDatabase(context); 250 } 251 252 /** 253 * Discard current recording file, release recorder and player 254 */ discardRecording()255 public void discardRecording() { 256 if ((STATE_RECORDING == mInternalState) && (null != mRecorder)) { 257 stopRecorder(); 258 } 259 260 if (mRecordFile != null && !mIsRecordingFileSaved) { 261 if (!mRecordFile.delete()) { 262 // deletion failed, possibly due to hot plug out SD card 263 Log.d(TAG, "discardRecording, delete file failed!"); 264 } 265 mRecordFile = null; 266 mRecordStartTime = 0; 267 mRecordTime = 0; 268 } 269 setState(STATE_IDLE); 270 } 271 272 /** 273 * Set the callback use to notify FM recorder state and error message 274 * 275 * @param listener the callback 276 */ registerRecorderStateListener(OnRecorderStateChangedListener listener)277 public void registerRecorderStateListener(OnRecorderStateChangedListener listener) { 278 mStateListener = listener; 279 } 280 281 /** 282 * Interface to notify FM recorder state and error message 283 */ 284 public interface OnRecorderStateChangedListener { 285 /** 286 * notify FM recorder state 287 * 288 * @param state current FM recorder state 289 */ onRecorderStateChanged(int state)290 void onRecorderStateChanged(int state); 291 292 /** 293 * notify FM recorder error message 294 * 295 * @param error error type 296 */ onRecorderError(int error)297 void onRecorderError(int error); 298 } 299 300 /** 301 * When recorder occur error, release player, notify error message, and 302 * update FM recorder state to idle 303 * 304 * @param mr The current recorder 305 * @param what The error message type 306 * @param extra The error message extra 307 */ 308 @Override onError(MediaRecorder mr, int what, int extra)309 public void onError(MediaRecorder mr, int what, int extra) { 310 Log.e(TAG, "onError, what = " + what + ", extra = " + extra); 311 stopRecorder(); 312 setError(ERROR_RECORDER_INTERNAL); 313 if (STATE_RECORDING == mInternalState) { 314 setState(STATE_IDLE); 315 } 316 } 317 318 @Override onInfo(MediaRecorder mr, int what, int extra)319 public void onInfo(MediaRecorder mr, int what, int extra) { 320 Log.d(TAG, "onInfo: what=" + what + ", extra=" + extra); 321 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || 322 what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 323 onError(mr, what, extra); 324 } 325 } 326 327 /** 328 * Reset FM recorder 329 */ resetRecorder()330 public void resetRecorder() { 331 if (mRecorder != null) { 332 mRecorder.release(); 333 mRecorder = null; 334 } 335 mRecordFile = null; 336 mRecordStartTime = 0; 337 mRecordTime = 0; 338 mInternalState = STATE_IDLE; 339 } 340 341 /** 342 * Notify error message to the callback 343 * 344 * @param error FM recorder error type 345 */ setError(int error)346 private void setError(int error) { 347 if (mStateListener != null) { 348 mStateListener.onRecorderError(error); 349 } 350 } 351 352 /** 353 * Notify FM recorder state message to the callback 354 * 355 * @param state FM recorder current state 356 */ setState(int state)357 private void setState(int state) { 358 mInternalState = state; 359 if (mStateListener != null) { 360 mStateListener.onRecorderStateChanged(state); 361 } 362 } 363 364 /** 365 * Save recording file info to database 366 * 367 * @param context The context 368 */ addRecordingToDatabase(final Context context)369 private void addRecordingToDatabase(final Context context) { 370 long curTime = System.currentTimeMillis(); 371 long modDate = mRecordFile.lastModified(); 372 Date date = new Date(curTime); 373 374 java.text.DateFormat dateFormatter = DateFormat.getDateFormat(context); 375 java.text.DateFormat timeFormatter = DateFormat.getTimeFormat(context); 376 String title = getRecordFileName(); 377 StringBuilder stringBuilder = new StringBuilder() 378 .append(FM_RECORD_FOLDER) 379 .append(" ") 380 .append(dateFormatter.format(date)) 381 .append(" ") 382 .append(timeFormatter.format(date)); 383 String artist = stringBuilder.toString(); 384 385 final int size = 9; 386 ContentValues cv = new ContentValues(size); 387 cv.put(MediaStore.Audio.Media.IS_MUSIC, 1); 388 cv.put(MediaStore.Audio.Media.TITLE, title); 389 cv.put(MediaStore.Audio.Media.DATA, mRecordFile.getAbsolutePath()); 390 final int oneSecond = 1000; 391 cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (curTime / oneSecond)); 392 cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / oneSecond)); 393 cv.put(MediaStore.Audio.Media.MIME_TYPE, RECORDING_FILE_TYPE); 394 cv.put(MediaStore.Audio.Media.ARTIST, artist); 395 cv.put(MediaStore.Audio.Media.ALBUM, RECORDING_FILE_SOURCE); 396 cv.put(MediaStore.Audio.Media.DURATION, mRecordTime); 397 398 int recordingId = addToAudioTable(context, cv); 399 if (recordingId < 0) { 400 // insert failed 401 return; 402 } 403 int playlistId = getPlaylistId(context); 404 if (playlistId < 0) { 405 // play list not exist, create FM Recording play list 406 playlistId = createPlaylist(context); 407 } 408 if (playlistId < 0) { 409 // insert playlist failed 410 return; 411 } 412 // insert item to FM recording play list 413 addToPlaylist(context, playlistId, recordingId); 414 // scan to update duration 415 MediaScannerConnection.scanFile(context, new String[] { mRecordFile.getPath() }, 416 null, null); 417 } 418 419 /** 420 * Get the play list ID 421 * @param context Current passed in Context instance 422 * @return The play list ID 423 */ getPlaylistId(final Context context)424 public static int getPlaylistId(final Context context) { 425 Cursor playlistCursor = context.getContentResolver().query( 426 MediaStore.Audio.Playlists.getContentUri("external"), 427 new String[] { 428 MediaStore.Audio.Playlists._ID 429 }, 430 MediaStore.Audio.Playlists.DATA + "=?", 431 new String[] { 432 FmUtils.getPlaylistPath(context) + RECORDING_FILE_SOURCE 433 }, 434 null); 435 int playlistId = -1; 436 if (null != playlistCursor) { 437 try { 438 if (playlistCursor.moveToFirst()) { 439 playlistId = playlistCursor.getInt(0); 440 } 441 } finally { 442 playlistCursor.close(); 443 } 444 } 445 return playlistId; 446 } 447 createPlaylist(final Context context)448 private int createPlaylist(final Context context) { 449 final int size = 1; 450 ContentValues cv = new ContentValues(size); 451 cv.put(MediaStore.Audio.Playlists.NAME, RECORDING_FILE_SOURCE); 452 Uri newPlaylistUri = context.getContentResolver().insert( 453 MediaStore.Audio.Playlists.getContentUri("external"), cv); 454 if (newPlaylistUri == null) { 455 Log.d(TAG, "createPlaylist, create playlist failed"); 456 return -1; 457 } 458 return Integer.valueOf(newPlaylistUri.getLastPathSegment()); 459 } 460 addToAudioTable(final Context context, final ContentValues cv)461 private int addToAudioTable(final Context context, final ContentValues cv) { 462 ContentResolver resolver = context.getContentResolver(); 463 int id = -1; 464 465 Cursor cursor = null; 466 467 try { 468 cursor = resolver.query( 469 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 470 new String[] { MediaStore.Audio.Media._ID }, 471 MediaStore.Audio.Media.DATA + "=?", 472 new String[] { mRecordFile.getPath() }, 473 null); 474 if (cursor != null && cursor.moveToFirst()) { 475 // Exist in database, just update it 476 id = cursor.getInt(0); 477 resolver.update(ContentUris.withAppendedId( 478 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id), 479 cv, 480 null, 481 null); 482 } else { 483 // insert new entry to database 484 Uri uri = context.getContentResolver().insert( 485 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cv); 486 if (uri != null) { 487 id = Integer.valueOf(uri.getLastPathSegment()); 488 } 489 } 490 } finally { 491 if (cursor != null) { 492 cursor.close(); 493 } 494 } 495 return id; 496 } 497 addToPlaylist(final Context context, final int playlistId, final int recordingId)498 private void addToPlaylist(final Context context, final int playlistId, final int recordingId) { 499 ContentResolver resolver = context.getContentResolver(); 500 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); 501 int order = 0; 502 Cursor cursor = null; 503 try { 504 cursor = resolver.query( 505 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 506 new String[] { MediaStore.Audio.Media._ID }, 507 MediaStore.Audio.Media.DATA + "=?", 508 new String[] { mRecordFile.getPath() }, 509 null); 510 if (cursor != null && cursor.moveToFirst()) { 511 // Exist in database, just update it 512 order = cursor.getCount(); 513 } 514 } finally { 515 if (cursor != null) { 516 cursor.close(); 517 } 518 } 519 ContentValues cv = new ContentValues(2); 520 cv.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, recordingId); 521 cv.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, order); 522 context.getContentResolver().insert(uri, cv); 523 } 524 stopRecorder()525 private void stopRecorder() { 526 synchronized (this) { 527 if (mRecorder != null) { 528 try { 529 mRecorder.stop(); 530 } catch (IllegalStateException ex) { 531 Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex); 532 setError(ERROR_RECORDER_INTERNAL); 533 } finally { 534 mRecorder.release(); 535 mRecorder = null; 536 } 537 } 538 } 539 } 540 } 541