需求:项目中有一期需要做通话录音自动上传功能需求。订单详情页面,用户拨打电话以后,自动将通话录音上传到云端服务器。原来的方式是通话完成,用户从本地文件夹中选择对应的录音文件,然后上传云端。因为业务人员感觉这个操作比较麻烦,因此希望能够自动上传通话录音。
调研: 自动上传通话录音功能包括两个方面:
1)通话录音的采集:用户拨打电话时,采集通话录音。这需要监听用户拨打订单电话的状态,包括电话的接通和挂断;
2)通话录音自动上传:通话结束后,将采集到的订单通话录音文件上传服务器。
这通话录音的采集有两种方案:
1)应用自身采集,这样能够无差错的建立起订单和通话录音文件的映射关系。
2)系统采集,然后抓取对应的通话录音文件。因为录音文件的生成是有系统负责,应用本身并不能进行控制。 这有一个问题:不同的系统,通话录音的保存路径不一样,录音文件的命名规则也不相同,不能够针对所有类型的设备进行处理,可以对一些特殊的设备进行处理。当然还存在安全性问题,不能准确无误的建立起订单和录音文件的映射关系。
实现:
因为方案一是一个通用方案,因此采用方案一方式实现。
1)监听用户的通话
/**
* 启动通话监听
*/
private void startCallListener() {
if (isRooted || isMIUI) {
//获得电话管理器
manager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
//为管理器设置监听器,监听电话的呼叫状态
phoneListener = new MyPhoneListener();
manager.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
}
/**
* 电话监听器
* 说明: 监听双向通话
*/
private class MyPhoneListener extends PhoneStateListener {
public void onCallStateChanged(int state, String incomingNumber) {
if(TextUtils.isEmpty(incomingNumber)) {
return;
}
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
//来电振动
LogUtils.d(TAG, "CALL_STATE_RINGING:" + incomingNumber);
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
LogUtils.d(TAG, "CALL_STATE_OFFHOOK:" + incomingNumber);
// 当接通电话开始通话时 可以进行录音
if (null != callRecordEvent
&& Utils.GetStringNoNil(callRecordEvent.phone).equals(incomingNumber)) {
//检查是否需要进行通话录音(只有订单通话才记录通话录音)
recordStartTime = System.currentTimeMillis();
phoneNumber = incomingNumber.replace(" ", "");
LogUtils.d(TAG, "onCallStateChanged recordStartTime:" + TimeUtil.format(TimeUtil.FORMAT_YYYY_MM_DD_HH_MM_SS, recordStartTime) + " phoneNumber: " + phoneNumber);
handler.sendEmptyMessage(MSG_START_CALL_RECORD);
}
break;
case TelephonyManager.CALL_STATE_IDLE:
LogUtils.d(TAG, "CALL_STATE_IDLE:" + incomingNumber);
//挂断电话时停止录音
handler.sendEmptyMessage(MSG_STOP_CALL_RECORD);
if (null != callRecordEvent && !TextUtils.isEmpty(incomingNumber)) {
recordStopTime = System.currentTimeMillis();
//录音文件事件复位等待下一次操作
callRecordEvent.phone = "";
//记录通话结束的手机号
phoneNumber = incomingNumber.replace(" ", "");
LogUtils.d(TAG, "onCallStateChanged recordStopTime:" + TimeUtil.format(TimeUtil.FORMAT_YYYY_MM_DD_HH_MM_SS, recordStopTime) + " phoneNumber: " + phoneNumber);
}
break;
}
}
}
/**
* 启动通话录音
* 说明: 只有Root设备才能启动通话录音功能
*/
private void startCallRecord() {
if (isRooted && !startFlag) {
// 创建录音器
createMediaRecorder();
// 开始记录录音
startRecording();
}
}
创建录音器:
/**
* 创建录音器对象
*/
private void createMediaRecorder() {
//1) Create MediaRecorder
mRecorder = new MediaRecorder();
// Set audio and video source and encoder
try {
//1代表单声道,2代表双声道(立体声)
mRecorder.setAudioChannels(2);
//2) 这两项需要放在setOutputFormat之前
mRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
} catch (Exception e) {
e.printStackTrace();
}
try {
//3) Set output file format(mp4格式)
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
} catch (Exception e) {
e.printStackTrace();
}
try {
//4) 这两项需要放在setOutputFormat之后
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
} catch (Exception e) {
e.printStackTrace();
}
// Set output file path
//初始化缓存目录
createOutputFile();
try {
//设置输出文件路径
mRecorder.setOutputFile(audioPath);
} catch (Exception e) {
e.printStackTrace();
}
}
开始通话录音
/**
* 开始录音
*/
private void startRecording() {
LogUtils.d(TAG, "bf mRecorder.prepare()");
try {
mRecorder.prepare();
} catch (Exception e) {
e.printStackTrace();
}
LogUtils.d(TAG, "af mRecorder.prepare()");
try {
LogUtils.d(TAG, "bf mRecorder.start()");
mRecorder.start(); // Recording is now started
} catch (Exception e) {
e.printStackTrace();
//启动出错
if(!isMIUI){
//MIUI支持录音文件抓取
ToastUtils.showToast(this, R.string.audio_start_failed, Toast.LENGTH_SHORT);
}
handler.sendEmptyMessage(MSG_START_CALL_RECORD_FAILED);
return;
}
//设置标签: 已经启动通话录音
startFlag = true;
LogUtils.d(TAG, "af mRecorder.start()");
LogUtils.d(TAG, "Start recording ...");
}
3)通话结束时停止通话录音开始上传
/**
* 结束通话录音
* 说明: 如果自动录音成功优先使用自动录音;
* 如果自动录音启动失败,且是小米系统,可以尝试抓取录音文件进行上传
* (1、有些手机未Root权限判断为已经Root;2、系统自身已经开启通话录音导致自动录音失败)。
*/
private void stopCallRecord() {
if (isRooted && startFlag) {
//如果是Root设备查找自我录音的文件
// (通话录音开启成功时使用自动录音的文件;否则使用小米手机的通话录音)
//停止录音设备
stopMediaRecorder();
//录音文件是非空的录音文件
if (FileHelper.getFileSize(audioPath) > 0) {
//录音结束开始启动通话录音文件上传
handler.sendEmptyMessage(MSG_UPLOAD_CALL_RECORD);
} else {
//空文件删除
FileHelper.deleteFile(audioPath);
}
// Set button status flag
startFlag = false;
} else if (isMIUI) {
//MIUI系统设备有自动通话录音功能抓取对应的通话录音文件
if(null != callRecordEvent) {
//只有在贷后自动录音处理才抓取录音文件
startFilterAudioFile(phoneNumber, recordStartTime,
callRecordEvent.son_order_id);
}
}
}
通话录音是一种比较危险权限,系统未Root时,除了系统自身授权,其他的应用是无法获取到这种权限的。
因此即使看到应用已经授权录音权限,如果手机未Root,自己采集通话录音不能成功。红米Note4是稳定版的系统,
因此应用不能采集到通话录音。
出现这种情况,有两种解决方案:
方式一:将手机Root。这是一种比较危险的操作。
方式二:应用不进行录音,有系统录音,应用抓取系统的录音文件进行上传。
对于未Root的手机,采用方式二进行处理。工作人员使用的手机是红米手机。小米手机的通话录音文件,
保存在一个固定的目录:
内存目录/MIUI/sound_recorder/call_rec,可以从目录中抓取通话录音文件。
小米手机通话录音文件的保存的文件名也很有特征:通话录音@手机号xxx_时间串.mp3,
我们可以根据手机号+时间串来抓取对应的通话录音文件。
/**
* 从小米文件夹中抓取对应的录音文件
*
* @param phoneNumber : 手机号
* @param recordStartTime : 录音文件开始时间
* @param son_order_id : 订单ID
*/
private void startFilterAudioFile(final String phoneNumber, final long recordStartTime,
String son_order_id) {
if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(son_order_id)) {
//手机号或者订单号不存在不需要上传
return;
}
//首先检查录音文件保存目录是否存在
if (!FileHelper.fileIsExists(Config.callRecordDir)) {
//对应的录音文件夹不存在
return;
}
//抓取对应的录音文件(手机号+通话开始时间)
File dir = new File(Config.callRecordDir);
if (!dir.isDirectory()) {
return;
}
//获取其中的子文件列表
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if (pathname.isDirectory()) {
//子文件夹不处理
return false;
}
//检测录音文件有效性
if (pathname.length() <= 0) {
//删除空白文件
FileHelper.deleteFile(pathname.getPath());
return false;
}
//过滤文件名日志
String simpleName = pathname.getName();
//去掉所有的空格字符
simpleName = simpleName.replace(" ", "");
if (!simpleName.contains(phoneNumber)) {
return false;
}
//过滤文件名中的时间
String dateStr = parseDate(simpleName);
if(TextUtils.isEmpty(dateStr)) {
return false;
}
//最早的时间
Date fileDate = DateUtils.strToDate(dateStr, TimeUtil.FORMAT_NO_SPLIT_YYYY_MM_DD_CHS_HH_MM_SS);
//录音文件时间一定在录音开始时间和录音结束时间的监听范围内
long fileTime = fileDate.getTime();
return fileTime >= 0 && fileTime >= recordStartTime && fileTime <= recordStopTime;
}
});
if (null == files || files.length <= 0) {
//未找到符合条件的文件
return;
}
//有多个文件符合条件时优先查找文件名刚好一致的文件
File destFile = null;
if(files.length > 1) {
//如果有多个文件符合条件优先获取时间刚好相同的文件
String str = TimeUtil.format(TimeUtil.FORMAT_NO_SPLIT_YYYY_MM_DD_CHS_HH_MM_SS, recordStartTime);
for (File file : files) {
if(file.getName().contains(str)) {
//找到一个完全匹配的文件
destFile = file;
break;
}
}
}
//如果没有找到完全匹配的文件默认采用过滤文件列表中第一个
if(null == destFile) {
destFile = files[0];
}
//开始上传录音文件
startUploadFile(destFile.getPath(), son_order_id);
}