赞
踩
各位大佬好,我又来记笔记了~~
公司又提新需求了,需要开发一个能通话(呼叫客户的手机号码)自动录音的模块。刚接触这个是蒙的,经过一番研究,可实现通话录音的方式大致有下面几种:
方案一:点击拨号时,调用系统的拨号功能,同时应用内注册通话广播,检测通话状态,接通、挂断来决定开始录音和停止录音,录音可以使用MediaRecorder和AudioRecorder。
优缺点:实现方式简单,开发容易。但是缺点也有,受Android系统版本影响大,每次打开应用都需要进设置页面开启“无障碍”权限才能录音(目前Android8.0的不用),录音对方的声音较小。不过适当优化下 也能用。
方案二:刷机,获取设备root权限,把应用修改为“系统”级别应用,就可以正常录制通话(跟手机自带的通话录音一样),具体怎么刷机自行百度
优缺点:参考手机自带的通话录音功能,效果还是非常好的,但是只能用于一些定制的设备。如正常的一些手机、pad用户就不得行了,因为客户不可能会去刷机来兼容我们的应用。
方案三: SIP软电话,集成第三方的VoIP网络电话,实现网络通话并录音,效果也还行。如linphone框架,也是本文要讲的。
优缺点:使用SIP软电话,前提是要有SIP服务器(网上有很多免费的SIP服务器),后面说具体的实现逻辑,通话录音还可以,双方声音都比较大。
方案四:呼叫时,点击开启系统的录音进行录制,返回我们应用时,把系统的录音文件拿出来展示或上传服务器,哈哈 最笨的方案了,适配主流的机型(前提是手机支持通话录音,获取录音文件的路径各机型适配一下)。
优缺点:兼容性差,不推荐了。
本文主要记录的是 《方案一》 和《方案三》,下面 只介绍关键步骤,详见文末demo
方案一:
大致步骤: 1、权限申请
2、注册广播,开启服务进行录音
3、开始拨号
4、查看通话记录,播放录音文件
需要的权限,项目全部权限在这了,有的可能用不到。
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
- <uses-permission android:name="android.permission.CALL_PHONE" />
- <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
- <uses-permission android:name="android.permission.READ_CALL_LOG" />
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission
- android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
- tools:ignore="ScopedStorage" />
- <uses-permission
- android:name="android.permission.MODIFY_PHONE_STATE"
- tools:ignore="ProtectedPermissions" />
- <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
- <uses-permission
- android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
- tools:ignore="ProtectedPermissions" />
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
注册广播:
AndroidManifest文件添加 PhoneStateListener和MediaRecorderService
- <receiver
- android:name=".callrecord.PhoneStateListener"
- android:enabled="true"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.PHONE_STATE" />
- </intent-filter>
- </receiver>
-
- <service
- android:name=".callrecord.MediaRecorderService"
- android:enabled="true"
- android:exported="true"
- android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
- <intent-filter>
- <action android:name="android.accessibilityservice.AccessibilityService" />
- </intent-filter>
-
- <meta-data
- android:name="android.accessibilityservice"
- android:resource="@xml/accessibility_service_config" />
- </service>
PhoneStateListener类:
- /**
- * @ClassName PhoneStateListener
- * @Description TODO
- * @Author HK.W 通话录音广播
- * @Date 2022/10/15 22:13
- */
- public class PhoneStateListener extends BroadcastReceiver {
- private static final String TAG = "通话状态监听";
- static boolean incoming_flag;
- private Context mContext;
-
- @Override
- public void onReceive(Context ctx, Intent intent) {
- mContext = ctx;
- String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
- Log.d(TAG, "通话状态:state:" + event);
- if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
- Log.d(TAG, "-->RINGING--正在响铃");
- incoming_flag = true;
- } else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
- Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通话");
- startService(ctx, intent);
- } else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
- Log.d(TAG, "-->EXTRA_STATE_IDLE--电话挂断--空闲");
- ctx.stopService(new Intent(ctx, MediaRecorderService.class));
- //AudioRecordUtil.getInstance().stopRecording();
- AudioRecorder.getInstance().stopRecord();
- }
-
- }
-
- private void startService(Context context, Intent intent) {
- Log.d(TAG, "-->startService--打开服务-检查权限");
- String[] PERMISSIONS = {Manifest.permission.RECORD_AUDIO,
- Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
- if (hasPermissions(context, PERMISSIONS)) {
- Log.d(TAG, "-->startService--打开服务-权限已打开");
- intent.setClass(context, MediaRecorderService.class);
- intent.putExtra("incoming_flag", incoming_flag);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- context.startForegroundService(intent);
- } else {
- context.startService(intent);
- }
- } else {
- Log.d(TAG, "-->startService--打开服务-权限未打开");
- }
- }
-
- public static boolean hasPermissions(Context context, String... permissions) {
- if (context != null && permissions != null) {
- for (String permission : permissions) {
- if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- }
- }
- return true;
- }
- }
MediaRecorderService类:
- public class MediaRecorderService extends AccessibilityService {
- private static final String TAG = "通话状态监听";
- NotificationManagerCompat notificationManager;
- private boolean incoming_flag;
- private String number;
-
- @Override
- public void onInterrupt() {
-
- }
-
- @Override
- public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
-
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent != null) {
- Log.d(TAG, "-->startService--进入录音服务");
- number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
- incoming_flag = intent.getBooleanExtra("incoming_flag", false);
- String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");
- AudioRecorder.getInstance().createDefaultAudio(phone);
- AudioRecorder.getInstance().startRecord(new RecordStreamListener() {
- @Override
- public void recordOfByte(byte[] data, int begin, int end) {
- Log.d(TAG, "data:" + data);
- }
- });
- notificationBuilder();
- }
- return START_STICKY;
- }
-
- private void notificationBuilder() {
- Log.d(TAG, "-->startService--录音服务--打开通知栏,让服务进入前台,避免被杀掉");
- if (Build.VERSION.SDK_INT >= 26) {
- String CHANNEL_ID = "my_channel_01";
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel title",
- NotificationManager.IMPORTANCE_DEFAULT);
- ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
- Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("")
- .setContentText("").build();
- startForeground(1, notification);
- } else {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "CHANNEL_ID")
- .setSmallIcon(R.mipmap.ic_launcher)
- .setContentTitle("Recording")
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setOngoing(true);
- notificationManager = NotificationManagerCompat.from(this);
- notificationManager.notify(1, builder.build());
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Log.d(TAG, "-->startService--录音服务--服务被销毁---onDestroy()");
- stopRecording();
- }
-
- private void stopRecording() {
- Log.d(TAG, "-->startService--录音服务--停止录音");
- if (Build.VERSION.SDK_INT >= 26) {
- stopForeground(true);
- } else {
- NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(1);
- }
- }
- }
功能相关页面截图:
拨号:
- private void callPhone(String phoneNumber) {
- Intent intentPhone = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" +
- phoneEt.getText().toString()));
- startActivity(intentPhone);
- }
开始录音
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent != null) {
- Log.d(TAG, "-->startService--进入录音服务");
- number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
- incoming_flag = intent.getBooleanExtra("incoming_flag", false);
- String phone = SpUtils.getInstance().getString(this, "phone", "Unknown");
- //开始录音
- AudioRecorder.getInstance().createDefaultAudio(phone);
- AudioRecorder.getInstance().startRecord(new RecordStreamListener() {
- @Override
- public void recordOfByte(byte[] data, int begin, int end) {
- Log.d(TAG, "data:" + data);
- }
- });
- notificationBuilder();
- }
- return START_STICKY;
- }
停止录音:
- public class PhoneStateListener extends BroadcastReceiver {
- private static final String TAG = "通话状态监听";
- static boolean incoming_flag;
- private Context mContext;
-
- @Override
- public void onReceive(Context ctx, Intent intent) {
- mContext = ctx;
- String event = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
- Log.d(TAG, "通话状态:state:" + event);
- if (event.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
- Log.d(TAG, "-->RINGING--正在响铃");
- incoming_flag = true;
- } else if (event.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
- Log.d(TAG, "-->EXTRA_STATE_OFFHOOK--正在通话");
- startService(ctx, intent);
- } else if (event.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
- Log.d(TAG, "-->EXTRA_STATE_IDLE--电话挂断--空闲");
- ctx.stopService(new Intent(ctx, MediaRecorderService.class));
- //AudioRecordUtil.getInstance().stopRecording();
-
- //为什么不在服务里面停止录音?有的机型挂断电话后没有马上销毁服务,所以在状态这里直接停止录音
- AudioRecorder.getInstance().stopRecord();
- }
- }
本文demo 录音文件保存在根目录anyi.phone/record 文件下。
获取通话记录对应的录音文件:
- /**
- * 获取录音文件路径 --通话记录
- */
- private List<RecordBean> getLocalRecord() {
- List<ContactsBean> contacts = readContacts();
- List<RecordBean> list = new ArrayList<>();
- JSONArray allFiles = getAllFiles("", "wav");
- //Log.d("allFiles", "allFiles:" + allFiles.toString());
- if (null != allFiles) {
- for (int i = 0; i < allFiles.length(); i++) {
- try {
- JSONObject jsonObject = allFiles.getJSONObject(i);
- String name = jsonObject.getString("name");
- String path = jsonObject.getString("path");
- String[] split1 = name.split("-");
- if (split1.length > 0) {
- RecordBean recordBean = new RecordBean();
- recordBean.setNumber(split1[0]);
- recordBean.setPath(path);
- recordBean.setDate(new SimpleDateFormat("HH:mm").format(new Date(Long.parseLong(split1[1]))));
- if (contacts.size() > 0) {
- for (ContactsBean b : contacts) {
- if (split1[0].equals(b.getNumber())) {
- recordBean.setCachedName(b.getName());
- }
- }
- } else {
- recordBean.setCachedName("未知");
- }
- list.add(recordBean);
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- Collections.reverse(list);
- return list;
- }
- return list;
- }
-
- public static JSONArray getAllFiles(String dirPath, String _type) {
- dirPath = "/storage/emulated/0/anyi.phone/record/";
- File f = new File(dirPath);
- if (!f.exists()) {//判断路径是否存在
- return null;
- }
- File[] files = f.listFiles();
- if (files == null) {//判断权限
- return null;
- }
- JSONArray fileList = new JSONArray();
- for (File _file : files) {//遍历目录
- if (_file.isFile() && (_file.getName().endsWith("amr")||_file.getName().endsWith("wav"))) {
- String _name = _file.getName();
- String filePath = _file.getAbsolutePath();//获取文件路径
- String fileName = _file.getName().substring(0, _name.length() - 4);//获取文件名
- try {
- JSONObject _fInfo = new JSONObject();
- _fInfo.put("name", fileName);
- _fInfo.put("path", filePath);
- fileList.put(_fInfo);
- } catch (Exception e) {
- }
- } else if (_file.isDirectory()) {//查询子目录
- //getAllFiles(_file.getAbsolutePath(), _type);
- } else {
- }
- }
- return fileList;
- }
播放:
- private void initPlay() {
- mediaPlayer = new MediaPlayer();
- }
-
- private void startPlay(String path) {
- if (TextUtils.isEmpty(path)) {
- Toast.makeText(this, "文件路径不存在", Toast.LENGTH_LONG).show();
- return;
- }
- mediaPlayer.reset(); //清空里面的其他歌曲
- try {
- mediaPlayer.setDataSource(path);
- mediaPlayer.prepare(); //准备就绪
- mediaPlayer.start(); //开始唱歌
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
方案三,SIP通话录音,linphone 为例,只调试了音频通话,视频通话未调试
前提准备
准备一个SIP服务器地址和一个账号密码。可以自己搭建SIP服务器或者网上找一个SIP服务器注册 一个账号密码。下面是网上找的资源,没试过。因为我们公司是购买的有SIP话机服务器的。
- 免费sip账号注册地址 http://serweb.iptel.org/user/reg/index.php
- 免费sip服务器 iptel.org
- 免费sip客户端 http://www.fring.com
正文:
1、把linphone-sdk-android-4.3.0-beta.aar包放在项目libs,提取码: nq6q。
2、配置文件注册服务:
- <service
- android:name=".linphone.LinphoneService"
- android:enabled="true"
- android:exported="true"
- android:label="@string/app_name" />
3.在启动页 启动SIP相关服务,
启动页:
- public class LauncherActivity extends AppCompatActivity {
-
- private static final String TAG = "XXPermissions";
- private Handler mHandler;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_launcher);
- mHandler = new Handler();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- getPermission();
- }
-
- private void getPermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- XXPermissions.with(this)
- .permission(allPermission)
- .request(new OnPermissionCallback() {
- @Override
- public void onGranted(List<String> permissions, boolean all) {
- if (all) {
- if (LinphoneService.isReady()) {
- onServiceReady();
- } else {
- startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));
- new ServiceWaitThread().start();
- }
- }
- }
-
- @Override
- public void onDenied(List<String> permissions, boolean never) {
- if (never) {
- Log.e(TAG, "onDenied:被永久拒绝授权,请手动授予权限 ");
- } else {
- Log.e(TAG, "onDenied: 权限获取失败");
- }
- }
- });
- } else {
- if (LinphoneService.isReady()) {
- onServiceReady();
- } else {
- startService(new Intent().setClass(LauncherActivity.this, LinphoneService.class));
- new ServiceWaitThread().start();
- }
- }
- }
-
- private void onServiceReady() {
- Intent intent = new Intent();
- intent.setClass(LauncherActivity.this, MainActivity.class);
- if (getIntent() != null && getIntent().getExtras() != null) {
- intent.putExtras(getIntent().getExtras());
- }
- intent.setAction(getIntent().getAction());
- intent.setType(getIntent().getType());
- startActivity(intent);
- }
-
- private class ServiceWaitThread extends Thread {
- public void run() {
- while (!LinphoneService.isReady()) {
- try {
- sleep(30);
- } catch (InterruptedException e) {
- throw new RuntimeException("waiting thread sleep() has been interrupted");
- }
- }
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- onServiceReady();
- }
- });
- }
- }
- }
首页activity onResume()方法中检测 账号是否注册,未注册跳转到注册页面:
- @Override
- protected void onResume() {
- super.onResume();
- Log.d(TAG, "onResume()");
- LinphoneService.getCore().addListener(mCoreListener);
- ProxyConfig proxyConfig = LinphoneService.getCore().getDefaultProxyConfig();
- if (proxyConfig != null) {
- updateLed(proxyConfig.getState());
- } else {
- startActivity(new Intent(this, ConfigureAccountActivity.class));
- }
- }
注册:
- /**
- * 注册
- */
- private void configureAccount() {
- mAccountCreator.setUsername(mUsername.getText().toString());
- mAccountCreator.setDomain(mDomain.getText().toString());
- mAccountCreator.setPassword(mPassword.getText().toString());
- switch (mTransport.getCheckedRadioButtonId()) {
- case R.id.transport_udp:
- mAccountCreator.setTransport(TransportType.Udp);
- break;
- case R.id.transport_tcp:
- mAccountCreator.setTransport(TransportType.Tcp);
- break;
- case R.id.transport_tls:
- mAccountCreator.setTransport(TransportType.Tls);
- break;
- }
- ProxyConfig cfg = mAccountCreator.createProxyConfig();
- LinphoneService.getCore().setDefaultProxyConfig(cfg);
- }
-
- public void listener(){
- mCoreListener = new CoreListenerStub() {
- /**
- * 监听注册是否成功
- * @param core
- * @param cfg
- * @param state
- * @param message
- */
- @Override
- public void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) {
- registerPr.setVisibility(View.GONE);
- if (state == RegistrationState.Ok) {
- finish();
- } else if (state == RegistrationState.Failed) {
- Toast.makeText(ConfigureAccountActivity.this, "Failure: " + message, Toast.LENGTH_LONG).show();
- }
- }
- };
- }
注册成功开始通话:
- private void sipCallIng() {
- Core core = LinphoneService.getCore();
- Address addressToCall = core.interpretUrl(phoneEt.getText().toString());
- CallParams params = core.createCallParams(null);
- params.enableVideo(false);
- if (addressToCall != null) {
- String filePath = AudioRecordUtil.getInstance().getFilename(phoneEt.getText().toString(), ".wav");
- android.util.Log.d("linPhone--", "开始呼叫--号码--filePath = " + filePath);
- //重要:通话前需要设置录音文件,要不不会录音,
- params.setRecordFile(filePath);
- core.inviteAddressWithParams(addressToCall, params);
-
- Intent intent = new Intent(getActivity(), CallActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
- }
开始录音:
-
- /**
- * ---通话接通--开始录音
- */
- private void startRecord() {
- android.util.Log.d("linPhone--", "接通或者拒绝");
- android.util.Log.d("linPhone--", "开始录音:录音地址:" + core.getRecordFile());
- call.startRecording();
- }
停止录音:
- /**
- * ---通话挂断--停止录音--销毁页面
- */
- private void stopRecord() {
- android.util.Log.d("linPhone--", "挂断,未接");
- android.util.Log.d("linPhone--", "停止录音");
-
- call.stopRecording();//停止录音
- finish();//挂断电话-销毁页面
- }
后面就是拿到录音文件播放,-----具体就不说了,
研究SIP也用了大量时间和下载了很多大佬的资源,也花费了很多积分,
so 想要demo的朋友们也希望支持一下,
demo需要积分下载,具体多少由平台分配。
本文demo成功实现了两种主流的通话录音方式,应该是能满足你们的业务需求的,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。