| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.fmradio; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.media.MediaPlayer; |
| import android.media.MediaRecorder; |
| import android.media.MediaScannerConnection; |
| import android.net.Uri; |
| import android.os.Environment; |
| import android.os.SystemClock; |
| import android.provider.MediaStore; |
| import android.text.format.DateFormat; |
| import android.util.Log; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Locale; |
| |
| /** |
| * This class provider interface to recording, stop recording, save recording |
| * file, play recording file |
| */ |
| public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener { |
| private static final String TAG = "FmRecorder"; |
| // file prefix |
| public static final String RECORDING_FILE_PREFIX = "FM"; |
| // file extension |
| public static final String RECORDING_FILE_EXTENSION = ".3gpp"; |
| // recording file folder |
| public static final String FM_RECORD_FOLDER = "FM Recording"; |
| private static final String RECORDING_FILE_TYPE = "audio/3gpp"; |
| private static final String RECORDING_FILE_SOURCE = "FM Recordings"; |
| // error type no sdcard |
| public static final int ERROR_SDCARD_NOT_PRESENT = 0; |
| // error type sdcard not have enough space |
| public static final int ERROR_SDCARD_INSUFFICIENT_SPACE = 1; |
| // error type can't write sdcard |
| public static final int ERROR_SDCARD_WRITE_FAILED = 2; |
| // error type recorder internal error occur |
| public static final int ERROR_RECORDER_INTERNAL = 3; |
| |
| // FM Recorder state not recording and not playing |
| public static final int STATE_IDLE = 5; |
| // FM Recorder state recording |
| public static final int STATE_RECORDING = 6; |
| // FM Recorder state playing |
| public static final int STATE_PLAYBACK = 7; |
| // FM Recorder state invalid, need to check |
| public static final int STATE_INVALID = -1; |
| |
| // use to record current FM recorder state |
| public int mInternalState = STATE_IDLE; |
| // the recording time after start recording |
| private long mRecordTime = 0; |
| // record start time |
| private long mRecordStartTime = 0; |
| // current record file |
| private File mRecordFile = null; |
| // record current record file is saved by user |
| private boolean mIsRecordingFileSaved = false; |
| // listener use for notify service the record state or error state |
| private OnRecorderStateChangedListener mStateListener = null; |
| // recorder use for record file |
| private MediaRecorder mRecorder = null; |
| |
| /** |
| * Start recording the voice of FM, also check the pre-conditions, if not |
| * meet, will return an error message to the caller. if can start recording |
| * success, will set FM record state to recording and notify to the caller |
| */ |
| public void startRecording(Context context) { |
| mRecordTime = 0; |
| |
| // Check external storage |
| if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { |
| Log.e(TAG, "startRecording, no external storage available"); |
| setError(ERROR_SDCARD_NOT_PRESENT); |
| return; |
| } |
| |
| String recordingSdcard = FmUtils.getDefaultStoragePath(); |
| // check whether have sufficient storage space, if not will notify |
| // caller error message |
| if (!FmUtils.hasEnoughSpace(recordingSdcard)) { |
| setError(ERROR_SDCARD_INSUFFICIENT_SPACE); |
| Log.e(TAG, "startRecording, SD card does not have sufficient space!!"); |
| return; |
| } |
| |
| // get external storage directory |
| File sdDir = new File(recordingSdcard); |
| File recordingDir = new File(sdDir, FM_RECORD_FOLDER); |
| // exist a file named FM Recording, so can't create FM recording folder |
| if (recordingDir.exists() && !recordingDir.isDirectory()) { |
| Log.e(TAG, "startRecording, a file with name \"FM Recording\" already exists!!"); |
| setError(ERROR_SDCARD_WRITE_FAILED); |
| return; |
| } else if (!recordingDir.exists()) { // try to create recording folder |
| boolean mkdirResult = recordingDir.mkdir(); |
| if (!mkdirResult) { // create recording file failed |
| setError(ERROR_RECORDER_INTERNAL); |
| return; |
| } |
| } |
| // create recording temporary file |
| long curTime = System.currentTimeMillis(); |
| Date date = new Date(curTime); |
| SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMddyyyy_HHmmss", |
| Locale.ENGLISH); |
| String time = simpleDateFormat.format(date); |
| StringBuilder stringBuilder = new StringBuilder(); |
| stringBuilder.append(time).append(RECORDING_FILE_EXTENSION); |
| String name = stringBuilder.toString(); |
| mRecordFile = new File(recordingDir, name); |
| try { |
| if (mRecordFile.createNewFile()) { |
| Log.d(TAG, "startRecording, createNewFile success with path " |
| + mRecordFile.getPath()); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "startRecording, IOException while createTempFile: " + e); |
| e.printStackTrace(); |
| setError(ERROR_SDCARD_WRITE_FAILED); |
| return; |
| } |
| // set record parameter and start recording |
| try { |
| mRecorder = new MediaRecorder(); |
| mRecorder.setOnErrorListener(this); |
| mRecorder.setOnInfoListener(this); |
| mRecorder.setAudioSource(MediaRecorder.AudioSource.RADIO_TUNER); |
| mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); |
| mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); |
| final int samplingRate = 44100; |
| mRecorder.setAudioSamplingRate(samplingRate); |
| final int bitRate = 128000; |
| mRecorder.setAudioEncodingBitRate(bitRate); |
| final int audiochannels = 2; |
| mRecorder.setAudioChannels(audiochannels); |
| mRecorder.setOutputFile(mRecordFile.getAbsolutePath()); |
| mRecorder.prepare(); |
| mRecordStartTime = SystemClock.elapsedRealtime(); |
| mRecorder.start(); |
| mIsRecordingFileSaved = false; |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "startRecording, IllegalStateException while starting recording!", e); |
| setError(ERROR_RECORDER_INTERNAL); |
| return; |
| } catch (IOException e) { |
| Log.e(TAG, "startRecording, IOException while starting recording!", e); |
| setError(ERROR_RECORDER_INTERNAL); |
| return; |
| } |
| setState(STATE_RECORDING); |
| } |
| |
| /** |
| * Stop recording, compute recording time and update FM recorder state |
| */ |
| public void stopRecording() { |
| if (STATE_RECORDING != mInternalState) { |
| Log.w(TAG, "stopRecording, called in wrong state!!"); |
| return; |
| } |
| |
| mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime; |
| stopRecorder(); |
| setState(STATE_IDLE); |
| } |
| |
| /** |
| * Compute the current record time |
| * |
| * @return The current record time |
| */ |
| public long getRecordTime() { |
| if (STATE_RECORDING == mInternalState) { |
| mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime; |
| } |
| return mRecordTime; |
| } |
| |
| /** |
| * Get FM recorder current state |
| * |
| * @return FM recorder current state |
| */ |
| public int getState() { |
| return mInternalState; |
| } |
| |
| /** |
| * Get current record file name |
| * |
| * @return The current record file name |
| */ |
| public String getRecordFileName() { |
| if (mRecordFile != null) { |
| String fileName = mRecordFile.getName(); |
| int index = fileName.indexOf(RECORDING_FILE_EXTENSION); |
| if (index > 0) { |
| fileName = fileName.substring(0, index); |
| } |
| return fileName; |
| } |
| return null; |
| } |
| |
| /** |
| * Save recording file with the given name, and insert it's info to database |
| * |
| * @param context The context |
| * @param newName The name to override default recording name |
| */ |
| public void saveRecording(Context context, String newName) { |
| if (mRecordFile == null) { |
| Log.e(TAG, "saveRecording, recording file is null!"); |
| return; |
| } |
| |
| File newRecordFile = new File(mRecordFile.getParent(), newName + RECORDING_FILE_EXTENSION); |
| boolean succuss = mRecordFile.renameTo(newRecordFile); |
| if (succuss) { |
| mRecordFile = newRecordFile; |
| } |
| mIsRecordingFileSaved = true; |
| // insert recording file info to database |
| addRecordingToDatabase(context); |
| } |
| |
| /** |
| * Discard current recording file, release recorder and player |
| */ |
| public void discardRecording() { |
| if ((STATE_RECORDING == mInternalState) && (null != mRecorder)) { |
| stopRecorder(); |
| } |
| |
| if (mRecordFile != null && !mIsRecordingFileSaved) { |
| if (!mRecordFile.delete()) { |
| // deletion failed, possibly due to hot plug out SD card |
| Log.d(TAG, "discardRecording, delete file failed!"); |
| } |
| mRecordFile = null; |
| mRecordStartTime = 0; |
| mRecordTime = 0; |
| } |
| setState(STATE_IDLE); |
| } |
| |
| /** |
| * Set the callback use to notify FM recorder state and error message |
| * |
| * @param listener the callback |
| */ |
| public void registerRecorderStateListener(OnRecorderStateChangedListener listener) { |
| mStateListener = listener; |
| } |
| |
| /** |
| * Interface to notify FM recorder state and error message |
| */ |
| public interface OnRecorderStateChangedListener { |
| /** |
| * notify FM recorder state |
| * |
| * @param state current FM recorder state |
| */ |
| void onRecorderStateChanged(int state); |
| |
| /** |
| * notify FM recorder error message |
| * |
| * @param error error type |
| */ |
| void onRecorderError(int error); |
| } |
| |
| /** |
| * When recorder occur error, release player, notify error message, and |
| * update FM recorder state to idle |
| * |
| * @param mr The current recorder |
| * @param what The error message type |
| * @param extra The error message extra |
| */ |
| @Override |
| public void onError(MediaRecorder mr, int what, int extra) { |
| Log.e(TAG, "onError, what = " + what + ", extra = " + extra); |
| stopRecorder(); |
| setError(ERROR_RECORDER_INTERNAL); |
| if (STATE_RECORDING == mInternalState) { |
| setState(STATE_IDLE); |
| } |
| } |
| |
| @Override |
| public void onInfo(MediaRecorder mr, int what, int extra) { |
| Log.d(TAG, "onInfo: what=" + what + ", extra=" + extra); |
| if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || |
| what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { |
| onError(mr, what, extra); |
| } |
| } |
| |
| /** |
| * Reset FM recorder |
| */ |
| public void resetRecorder() { |
| if (mRecorder != null) { |
| mRecorder.release(); |
| mRecorder = null; |
| } |
| mRecordFile = null; |
| mRecordStartTime = 0; |
| mRecordTime = 0; |
| mInternalState = STATE_IDLE; |
| } |
| |
| /** |
| * Notify error message to the callback |
| * |
| * @param error FM recorder error type |
| */ |
| private void setError(int error) { |
| if (mStateListener != null) { |
| mStateListener.onRecorderError(error); |
| } |
| } |
| |
| /** |
| * Notify FM recorder state message to the callback |
| * |
| * @param state FM recorder current state |
| */ |
| private void setState(int state) { |
| mInternalState = state; |
| if (mStateListener != null) { |
| mStateListener.onRecorderStateChanged(state); |
| } |
| } |
| |
| /** |
| * Save recording file info to database |
| * |
| * @param context The context |
| */ |
| private void addRecordingToDatabase(final Context context) { |
| long curTime = System.currentTimeMillis(); |
| long modDate = mRecordFile.lastModified(); |
| Date date = new Date(curTime); |
| |
| java.text.DateFormat dateFormatter = DateFormat.getDateFormat(context); |
| java.text.DateFormat timeFormatter = DateFormat.getTimeFormat(context); |
| String title = getRecordFileName(); |
| StringBuilder stringBuilder = new StringBuilder() |
| .append(FM_RECORD_FOLDER) |
| .append(" ") |
| .append(dateFormatter.format(date)) |
| .append(" ") |
| .append(timeFormatter.format(date)); |
| String artist = stringBuilder.toString(); |
| |
| final int size = 9; |
| ContentValues cv = new ContentValues(size); |
| cv.put(MediaStore.Audio.Media.IS_MUSIC, 1); |
| cv.put(MediaStore.Audio.Media.TITLE, title); |
| cv.put(MediaStore.Audio.Media.DATA, mRecordFile.getAbsolutePath()); |
| final int oneSecond = 1000; |
| cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (curTime / oneSecond)); |
| cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / oneSecond)); |
| cv.put(MediaStore.Audio.Media.MIME_TYPE, RECORDING_FILE_TYPE); |
| cv.put(MediaStore.Audio.Media.ARTIST, artist); |
| cv.put(MediaStore.Audio.Media.ALBUM, RECORDING_FILE_SOURCE); |
| cv.put(MediaStore.Audio.Media.DURATION, mRecordTime); |
| |
| int recordingId = addToAudioTable(context, cv); |
| if (recordingId < 0) { |
| // insert failed |
| return; |
| } |
| int playlistId = getPlaylistId(context); |
| if (playlistId < 0) { |
| // play list not exist, create FM Recording play list |
| playlistId = createPlaylist(context); |
| } |
| if (playlistId < 0) { |
| // insert playlist failed |
| return; |
| } |
| // insert item to FM recording play list |
| addToPlaylist(context, playlistId, recordingId); |
| // scan to update duration |
| MediaScannerConnection.scanFile(context, new String[] { mRecordFile.getPath() }, |
| null, null); |
| } |
| |
| /** |
| * Get the play list ID |
| * @param context Current passed in Context instance |
| * @return The play list ID |
| */ |
| public static int getPlaylistId(final Context context) { |
| Cursor playlistCursor = context.getContentResolver().query( |
| MediaStore.Audio.Playlists.getContentUri("external"), |
| new String[] { |
| MediaStore.Audio.Playlists._ID |
| }, |
| MediaStore.Audio.Playlists.DATA + "=?", |
| new String[] { |
| FmUtils.getPlaylistPath(context) + RECORDING_FILE_SOURCE |
| }, |
| null); |
| int playlistId = -1; |
| if (null != playlistCursor) { |
| try { |
| if (playlistCursor.moveToFirst()) { |
| playlistId = playlistCursor.getInt(0); |
| } |
| } finally { |
| playlistCursor.close(); |
| } |
| } |
| return playlistId; |
| } |
| |
| private int createPlaylist(final Context context) { |
| final int size = 1; |
| ContentValues cv = new ContentValues(size); |
| cv.put(MediaStore.Audio.Playlists.NAME, RECORDING_FILE_SOURCE); |
| Uri newPlaylistUri = context.getContentResolver().insert( |
| MediaStore.Audio.Playlists.getContentUri("external"), cv); |
| if (newPlaylistUri == null) { |
| Log.d(TAG, "createPlaylist, create playlist failed"); |
| return -1; |
| } |
| return Integer.valueOf(newPlaylistUri.getLastPathSegment()); |
| } |
| |
| private int addToAudioTable(final Context context, final ContentValues cv) { |
| ContentResolver resolver = context.getContentResolver(); |
| int id = -1; |
| |
| Cursor cursor = null; |
| |
| try { |
| cursor = resolver.query( |
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, |
| new String[] { MediaStore.Audio.Media._ID }, |
| MediaStore.Audio.Media.DATA + "=?", |
| new String[] { mRecordFile.getPath() }, |
| null); |
| if (cursor != null && cursor.moveToFirst()) { |
| // Exist in database, just update it |
| id = cursor.getInt(0); |
| resolver.update(ContentUris.withAppendedId( |
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id), |
| cv, |
| null, |
| null); |
| } else { |
| // insert new entry to database |
| Uri uri = context.getContentResolver().insert( |
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cv); |
| if (uri != null) { |
| id = Integer.valueOf(uri.getLastPathSegment()); |
| } |
| } |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| return id; |
| } |
| |
| private void addToPlaylist(final Context context, final int playlistId, final int recordingId) { |
| ContentResolver resolver = context.getContentResolver(); |
| Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); |
| int order = 0; |
| Cursor cursor = null; |
| try { |
| cursor = resolver.query( |
| MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, |
| new String[] { MediaStore.Audio.Media._ID }, |
| MediaStore.Audio.Media.DATA + "=?", |
| new String[] { mRecordFile.getPath() }, |
| null); |
| if (cursor != null && cursor.moveToFirst()) { |
| // Exist in database, just update it |
| order = cursor.getCount(); |
| } |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| ContentValues cv = new ContentValues(2); |
| cv.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, recordingId); |
| cv.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, order); |
| context.getContentResolver().insert(uri, cv); |
| } |
| |
| private void stopRecorder() { |
| synchronized (this) { |
| if (mRecorder != null) { |
| try { |
| mRecorder.stop(); |
| } catch (IllegalStateException ex) { |
| Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex); |
| setError(ERROR_RECORDER_INTERNAL); |
| } finally { |
| mRecorder.release(); |
| mRecorder = null; |
| } |
| } |
| } |
| } |
| } |