通过FmMainActivity的 Start recording 菜单选项即可进入FmRecordActivity直接进行录音,当FmRecordActivity销毁时,录音则停止。如果要实现后台也可以录音,只需保证录音的状态从FmService中读取并且Activity销毁时不停止录音即可。
下面从界面管理,后台Service以及录音实现三部分分析
进入Record界面执行过程
FmRecordActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
mContext = getApplicationContext();
mFragmentManager = getFragmentManager();
setContentView(R.layout.fm_record_activity);
mMintues = (TextView) findViewById(R.id.minutes);
mSeconds = (TextView) findViewById(R.id.seconds);
mFrequency = (TextView) findViewById(R.id.frequency);
mStationInfoLayout = findViewById(R.id.station_name_rt);
mStationName = (TextView) findViewById(R.id.station_name);
mRadioText = (TextView) findViewById(R.id.radio_text);
mStopRecordButton = (Button) findViewById(R.id.btn_stop_record);
mStopRecordButton.setEnabled(false);
mStopRecordButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Stop recording and wait service notify stop record state to show dialog
mService.stopRecordingAsync();
}
});
mPlayIndicator = (FmVisualizerView) findViewById(R.id.fm_play_indicator);
if (savedInstanceState != null) {
mCurrentStation = savedInstanceState.getInt(FmStation.CURRENT_STATION);
mIsActivityRecreate = true;
} else {
Intent intent = getIntent();
mCurrentStation = intent.getIntExtra(FmStation.CURRENT_STATION,
FmUtils.DEFAULT_STATION);
mRecordState = intent.getIntExtra("last_record_state", FmRecorder.STATE_INVALID);
}
// 绑定FmService来与FmService交互
bindService(new Intent(this, FmService.class), mServiceConnection,
Context.BIND_AUTO_CREATE);
// wangyannan begin
// should called after getting lateset state
//updateUi();
if(FM_STOP_RECORDING.equals(getIntent().getAction())){
needStopRecording = true;
}
// wangyannan end
}
绑定成功会调用onServiceConnected
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, android.os.IBinder service) {
// 返回的Binder对象通过getService方法得到FmService对象,从而实现调用其方法达到交互
mService = ((FmService.ServiceBinder) service).getService();
Log.d(TAG, "onServiceConnected " + mService);
if (mIsPermissionsRevoked == true) {
Log.w(TAG, "onServiceConnected: return due to permissions revoked in bg.");
return;
}
mService.registerFmRadioListener(mFmListener);
// wangyannan begin
// 判断是否点击通知中的停止按钮,如果点击则停止录音
if(needStopRecording) {
if(!isStopRecording()){
mService.stopRecordingAsync();
}
}
// mCurrentStation should read from FMService when it is default station.
// 从FmService刷新当前录音频道
if(mCurrentStation == FmUtils.DEFAULT_STATION) {
mCurrentStation = mService.getFrequency();
}
Log.d(TAG, "wangyannan onServiceConnected, mCurrentStation = " + mCurrentStation);
// wangyannan end
mService.setFmRecordActivityForeground(!mIsInBackground);
// When Activity re-launch, need get latest record state from service.
// wangyannan begin
// 从FmService中刷新当前录音状态在FmRecorder中定义
if (mIsActivityRecreate || (mRecordState == FmRecorder.STATE_INVALID && mService.getRecorderState() != FmRecorder.STATE_IDLE)) {
mRecordState = mService.getRecorderState();
}
Log.d(TAG, "wangyannan onServiceConnected, mRecordState = " + mRecordState);
// when get latest status , update ui
// 获取到当前录音的最新状态后更新界面信息
updateUi();
removeNotification();
// wangyannan end
// 1. If have stopped recording, we need check whether need show save dialog again.
// Because when stop recording in background, we need show it when switch to foreground.
if (isStopRecording()) {
if (!isSaveDialogShown()) {
//弹出是否保存录音对话框
showSaveDialog();
}
return;
}
// 2. If not start recording, start it directly, this case happen when start this
// activity from main fm activity.
if (!isStartRecording()) {
// 开始录音
mService.startRecordingAsync();
}
mPlayIndicator.startAnimation();
mStopRecordButton.setEnabled(true);
mHandler.removeMessages(FmListener.MSGID_REFRESH);
// 发送MSGID_REFRESH消息更新录音界面的事件信息
mHandler.sendEmptyMessage(FmListener.MSGID_REFRESH);
};
@Override
public void onServiceDisconnected(android.content.ComponentName name) {
mService = null;
};
};
更新界面录音时间信息
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "handleMessage: msg_what = " + msg.what);
switch (msg.what) {
case FmListener.MSGID_REFRESH:
if (mService != null) {
// 通过FmService得到录音的事件
long recordTimeInMillis = mService.getRecordTime();
long recordTimeInSec = recordTimeInMillis / 1000L;
// 计算分秒更新界面事件信息
mMintues.setText(addPaddingForString(recordTimeInSec / TIME_BASE));
mSeconds.setText(addPaddingForString(recordTimeInSec % TIME_BASE));
// 每1s刷新都需要检查当前空间和Fm播放的状态
checkStorageSpaceAndStop();
}
mHandler.sendEmptyMessageDelayed(FmListener.MSGID_REFRESH, 1000);
break;
.....
每1s更新界面显示时间,并检测Fm是否播放和空间是否够
private void checkStorageSpaceAndStop() {
Log.d(TAG, "checkStorageSpaceAndStop");
long recordTimeInMillis = mService.getRecordTime();
long recordTimeInSec = recordTimeInMillis / 1000L;
// Check storage free space
String recordingSdcard = FmUtils.getDefaultStoragePath();
// 通过FmUtils判断存储空间是否足够
if (!FmUtils.hasEnoughSpace(recordingSdcard)) {
// Need to record more than 1s.
// Avoid calling MediaRecorder.stop() before native record starts.
if (recordTimeInSec >= 1) {
// Insufficient storage
mService.stopRecordingAsync();
Toast.makeText(FmRecordActivity.this,
R.string.toast_sdcard_insufficient_space,
Toast.LENGTH_SHORT).show();
}
// 判断Fm如果不在播放则停止录音
} else if (mService.getPowerStatus() != FmService.POWER_UP) {
// Need to record more than 1s.
// Avoid calling MediaRecorder.stop() before native record starts.
if (recordTimeInSec >= 1) {
mService.stopRecordingAsync();
}
}
}
FmService在FmMainActivity通过start的方式已经启动,只有停止Fm播放,并返回键退出时才会stopService,其他情况即使是移除FmRadio在任务栈中,该Service会通过通知设置为前台进程一直运行。
如果Service处于后台,我们只能通过发送广播等方式与其交互,只有通过BinderService的方式才可以通过FmService对象直接与其交互。
还有一点需要注意的是Service中也不能执行耗时操作,否则会引起ANR,我们可以通过子线程的方式执行耗时操作,如果需要和Service及交互,则主线程中保留子线程的Handler即可,通过消息机制可完美与子线程交互,在FmService中是通过HandlerThread的方式实现。
HandlerThread handlerThread = new HandlerThread("FmRadioServiceThread");
handlerThread.start();
mFmServiceHandler = new FmRadioServiceHandler(handlerThread.getLooper());
HandlerThread会开启新的线程,子线程的Handler就是FmRadioServiceHandler,因为FmRadioServiceHandler的Looper对象是子线程的Looper。
mService.startRecordingAsync();
/**
* Start recording
*/
public void startRecordingAsync() {
Log.d(TAG, "startRecordingAsync");
mFmServiceHandler.removeMessages(FmListener.MSGID_STARTRECORDING_FINISHED);
mFmServiceHandler.sendEmptyMessage(FmListener.MSGID_STARTRECORDING_FINISHED);
}
/**
* The background handler
*/
class FmRadioServiceHandler extends Handler {
public FmRadioServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
Bundle bundle;
boolean isPowerup = false;
boolean isSwitch = true;
Log.d(TAG, "handleMessage: " + msg.what);
switch (msg.what) {
...
/********** recording **********/
case FmListener.MSGID_STARTRECORDING_FINISHED:
startRecording();
break;
...
startRecording在子线程中运行
private void startRecording() {
Log.d(TAG, "startRecording");
// 获取默认存储路径
sRecordingSdcard = FmUtils.getDefaultStoragePath();
if (sRecordingSdcard == null || sRecordingSdcard.isEmpty()) {
Log.d(TAG, "startRecording, may be no sdcard");
onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT);
return;
}
if (mFmRecorder == null) {
mFmRecorder = new FmRecorder();
mFmRecorder.registerRecorderStateListener(FmService.this);
}
if (isSdcardReady(sRecordingSdcard)) {
// 调用FmRecorder对音频录音的封装类进行录音
mFmRecorder.startRecording(mContext);
} else {
onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT);
}
}
停止录音调用了mService.stopRecordingAsync();
,流程和开始录音流程相同。
除了分析录音流程,这里再补充下FmService是如何将自己的状态通知给其他需要获取状态的界面。这里使用的当然是回调,其是通过List集合维护了所有注册接口监听器的对象,待状态需要通知给关注者时调用监听器中的callback方法即可。该Listener设计如下
FmListener
/**
* Activity connect FMRadio service should implements this interface to update
* ui or status
*/
public interface FmListener {
/**
* directly call back from service to activity
*/
// FM RDS station changed
int LISTEN_RDSSTATION_CHANGED = 0x00100010;
// FM PS information changed
int LISTEN_PS_CHANGED = 0x00100011;
// FM RT information changed
int LISTEN_RT_CHANGED = 0x00100100;
// FM Record state changed
int LISTEN_RECORDSTATE_CHANGED = 0x00100101; // 1048833
// FM record error occur
int LISTEN_RECORDERROR = 0x00100110; // 1048848
// FM record mode change
int LISTEN_RECORDMODE_CHANGED = 0x00100111; // 4018849
// FM Record state changed
int LISTEN_SPEAKER_MODE_CHANGED = 0x00101000; // 1052672
// Bundle keys
String SWITCH_ANTENNA_VALUE = "switch_antenna_value";
String CALLBACK_FLAG = "callback_flag";
String KEY_IS_SWITCH_ANTENNA = "key_is_switch_antenna";
String KEY_BT_STATE = "key_bt_state";
String KEY_IS_TUNE = "key_is_tune";
String KEY_TUNE_TO_STATION = "key_tune_to_station";
String KEY_IS_SEEK = "key_is_seek";
String KEY_SEEK_TO_STATION = "key_seek_to_station";
String KEY_IS_SCAN = "key_is_scan";
String KEY_RDS_STATION = "key_rds_station";
String KEY_PS_INFO = "key_ps_info";
String KEY_RT_INFO = "key_rt_info";
String KEY_STATION_NUM = "key_station_num";
// Audio focus related
String KEY_AUDIOFOCUS_CHANGED = "key_audiofocus_changed";
// Recording
String KEY_RECORDING_STATE = "key_is_recording_state";
String KEY_RECORDING_ERROR_TYPE = "key_recording_error_type";
String KEY_IS_RECORDING_MODE = "key_is_recording_mode";
String KEY_RECORDING_NAME = "recording_name";
// For change speaker/earphone mode
String KEY_IS_SPEAKER_MODE = "key_is_speaker_mode";
/**
* handle message: call back from service to activity
*/
// Message to handle
int MSGID_UPDATE_RDS = 1;
int MSGID_UPDATE_CURRENT_STATION = 2;
int MSGID_ANTENNA_UNAVAILABE = 3;
int MSGID_SWITCH_ANTENNA = 4;
int MSGID_SET_RDS_FINISHED = 5;
int MSGID_SET_CHANNEL_FINISHED = 6;
int MSGID_SET_MUTE_FINISHED = 7;
// Fm main
int MSGID_POWERUP_FINISHED = 9;
int MSGID_POWERDOWN_FINISHED = 10;
int MSGID_FM_EXIT = 11;
int MSGID_SCAN_CANCELED = 12;
int MSGID_SCAN_FINISHED = 13;
int MSGID_AUDIOFOCUS_FAILED = 14;
int MSGID_TUNE_FINISHED = 15;
int MSGID_SEEK_FINISHED = 16;
int MSGID_ACTIVE_AF_FINISHED = 18;
// Recording
int MSGID_RECORD_STATE_CHANGED = 19;
int MSGID_RECORD_ERROR = 20;
int MSGID_RECORD_MODE_CHANED = 21;
int MSGID_STARTRECORDING_FINISHED = 22;
int MSGID_STOPRECORDING_FINISHED = 23;
int MSGID_STARTPLAYBACK_FINISHED = 24;
int MSGID_STOPPLAYBACK_FINISHED = 25;
int MSGID_SAVERECORDING_FINISHED = 26;
// Audio focus related
int MSGID_AUDIOFOCUS_CHANGED = 30;
int NOT_AUDIO_FOCUS = 33;
int MSGID_BT_STATE_CHANGED = 34;
// For refresh time
int MSGID_REFRESH = 101;
int UPDATE_NOTIFICATION = 102;
// For EM
String KEY_IS_POWER_UP = "key_is_power_up";
/**
* Call back method to activity from service
*/
void onCallBack(Bundle bundle);
}
该Listener包含了各种状态值,以及回调方法onCallBack
这里已通知录音出错为例
Bundle bundle = new Bundle(2);
bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RECORDERROR);
bundle.putInt(FmListener.KEY_RECORDING_ERROR_TYPE, mRecorderErrorType);
notifyActivityStateChanged(bundle);
private ArrayList mRecords = new ArrayList();
/**
* Call back from service to activity
*
* @param bundle The message to activity
*/
private void notifyActivityStateChanged(Bundle bundle) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "notifyActivityStateChanged: " + mRecords.size());
}
// 遍历mRecords集合
if (!mRecords.isEmpty()) {
synchronized (mRecords) {
Iterator iterator = mRecords.iterator();
while (iterator.hasNext()) {
Record record = (Record) iterator.next();
FmListener listener = record.mCallback;
if (listener == null) {
iterator.remove();
return;
}
// 对注册该监听器的对象回调onCallBack方法
listener.onCallBack(bundle);
}
}
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "notifyActivityStateChanged: " + mRecords.isEmpty());
}
}
}
Record只是对FmListener的简单封装,用来唯一标识注册的FmListener
/**
* FM Radio listener record
*/
private static class Record {
int mHashCode; // hash code
FmListener mCallback; // call back
}
注册方法
/**
* Register FM Radio listener, activity get service state should call this
* method register FM Radio listener
*
* @param callback FM Radio listener
*/
public void registerFmRadioListener(FmListener callback) {
synchronized (mRecords) {
// register callback in AudioProfileService, if the callback is
// exist, just replace the event.
Record record = null;
int hashCode = callback.hashCode();
final int n = mRecords.size();
for (int i = 0; i < n; i++) {
record = mRecords.get(i);
if (hashCode == record.mHashCode) {
return;
}
}
record = new Record();
record.mHashCode = hashCode;
record.mCallback = callback;
mRecords.add(record);
}
}
这里已FmRecordActivity注册该监听器为例,在服务连接上后会进行注册mService.registerFmRadioListener(mFmListener);
,mFmListener为下
// Service listener
private final FmListener mFmListener = new FmListener() {
@Override
public void onCallBack(Bundle bundle) {
int flag = bundle.getInt(FmListener.CALLBACK_FLAG);
if (flag == FmListener.MSGID_FM_EXIT) {
mHandler.removeCallbacksAndMessages(null);
finish();
}
// remove tag message first, avoid too many same messages in queue.
Message msg = mHandler.obtainMessage(flag);
msg.setData(bundle);
mHandler.removeMessages(flag);
mHandler.sendMessage(msg);
}
};
FmRecordActivity将接收到信息,封装为消息发送给mHandler交由其处理. 上一步携带的消息为LISTEN_RECORDERROR,故会走下面case分支
case FmListener.LISTEN_RECORDERROR:
Bundle bundle = msg.getData();
int errorType = bundle.getInt(FmListener.KEY_RECORDING_ERROR_TYPE);
handleRecordError(errorType);
break;
对于其他FmMainActivity和FmFavoriteActivity界面处理机制是相同的。
/**
* 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) {
Log.d(TAG, "startRecording");
mRecordTime = 0;
// 1. 检查存储是否挂载
// Check external storage
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
Log.e(TAG, "startRecording, no external storage available");
setError(ERROR_SDCARD_NOT_PRESENT);
return;
}
// 2. 检查存储大小是否足够
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;
}
// 3. 创建录音目录
// 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;
}
}
// 4. 创建临时的录音文件,通过日期和后缀名生成临时录音文件的名字
// 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;
}
// 5. 通过MediaRecorder进行录音
// 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;
}
// 6. 设置当前录音状态
setState(STATE_RECORDING);
}
MediaRecorder具体使用在++MediaRecorder和AudioRecorder比较篇记录++
/**
* Stop recording, compute recording time and update FM recorder state
*/
public void stopRecording() {
Log.d(TAG, "stopRecording, mInternalState = " + mInternalState);
if (STATE_RECORDING != mInternalState) {
Log.w(TAG, "stopRecording, called in wrong state: state = " + mInternalState);
return;
}
// 记录录音时间
mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime;
stopRecorder(); // 停止录音
// 设置录音状态为IDLE
setState(STATE_IDLE);
}
private void stopRecorder() {
Log.d(TAG, "stopRecorder");
synchronized (this) {
if (mRecorder != null) {
try {
// MediaRecorder停止录音
mRecorder.stop();
} catch (IllegalStateException ex) {
Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex);
setError(ERROR_RECORDER_INTERNAL);
} catch (RuntimeException e) {
Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + e);
setError(ERROR_RECORDER_INTERNAL);
} finally {
mRecorder.release();
setState(STATE_IDLE);
mRecorder = 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) {
...
// 对录音临时文件按照设置的名称重命名
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);
}
录音状态有以下4种
// 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;