blob: 18a9d0006d9b13f8672ce6fa56ea285aca06fd8c [file] [log] [blame]
/*
* 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;
}
}
}
}
}