项目中需要实现基于Android 6.0 的双向通话自动录音功能,在查阅相关android电话状态监听文章以及git上的开源录音项目后,整理出此文


首先,介绍一下android 电话状态的监听(来电和去电):

http://www.cnblogs.com/haowenbiao/archive/2012/08/15/2639579.html

实现手机电话状态的监听,主要依靠两个类:
TelephoneMangerPhoneStateListener
TelephonseManger提供了取得手机基本服务的信息的一种方式。因此应用程序可以使用TelephonyManager来探测手机基本服务的情况。应用程序可以注册listener来监听电话状态的改变。
我们不能对TelephonyManager进行实例化,只能通过获取服务的形式:

Context.getSystemService(Context.TELEPHONY_SERVICE);

注意:对手机的某些信息进行读取是需要一定许可(permission)的。

主要静态成员常量:(它们对应PhoneStateListener.LISTEN_CALL_STATE所监听到的内容)

int CALL_STATE_IDLE   //空闲状态,没有任何活动。int CALL_STATE_OFFHOOK  //摘机状态,至少有个电话活动。该活动或是拨打(dialing)或是通话,或是 on hold。并且没有电话是ringing or waitingint CALL_STATE_RINGING  //来电状态,电话铃声响起的那段时间或正在通话又来新电,新来电话不得不等待的那段时间。

项目中使用服务来监听通话状态,所以需要弄清楚手机通话状态在广播中的对应值:

EXTRA_STATE_IDLE //它在手机通话状态改变的广播中,用于表示CALL_STATE_IDLE状态,即空闲状态。EXTRA_STATE_OFFHOOK //它在手机通话状态改变的广播中,用于表示CALL_STATE_OFFHOOK状态,即摘机状态。EXTRA_STATE_RINGING //它在手机通话状态改变的广播中,用于表示CALL_STATE_RINGING状态,即来电状态ACTION_PHONE_STATE_CHANGED //在广播中用ACTION_PHONE_STATE_CHANGED这个Action来标示通话状态改变的广播(intent)。
//注:需要许可READ_PHONE_STATE。String EXTRA_INCOMING_NUMBER  //在手机通话状态改变的广播,用于从extra取来电号码。String EXTRA_STATE  //在通话状态改变的广播,用于从extra取来通话状态。

如何实现电话监听呢?
Android在电话状态改变是会发送action为android.intent.action.PHONE_STATE的广播,而拨打电话时会发送action为

public static final String ACTION_NEW_OUTGOING_CALL ="android.intent.action.NEW_OUTGOING_CALL";

的广播。通过自定义广播接收器,接受上述两个广播便可。

下面给出Java代码:(其中的Toast均为方便测试而添加)

package com.example.hgx.phoneinfo60.Recording;import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.widget.Toast;/*** Created by hgx on 2016/6/13.*/
public class PhoneCallReceiver extends BroadcastReceiver {private int lastCallState  = TelephonyManager.CALL_STATE_IDLE;private boolean isIncoming = false;private static String contactNum;Intent audioRecorderService;public PhoneCallReceiver() {}@Overridepublic void onReceive(Context context, Intent intent) {//如果是去电if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)){contactNum = intent.getExtras().getString(Intent.EXTRA_PHONE_NUMBER);}else //android.intent.action.PHONE_STATE.查了下android文档,貌似没有专门用于接收来电的action,所以,非去电即来电.{String state = intent.getExtras().getString(TelephonyManager.EXTRA_STATE);String phoneNumber = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);int stateChange = 0;if (state.equals(TelephonyManager.EXTRA_STATE_IDLE)){//空闲状态stateChange =TelephonyManager.CALL_STATE_IDLE;if (isIncoming){onIncomingCallEnded(context,phoneNumber);}else {onOutgoingCallEnded(context,phoneNumber);}}else if (state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){//摘机状态stateChange = TelephonyManager.CALL_STATE_OFFHOOK;if (lastCallState != TelephonyManager.CALL_STATE_RINGING){//如果最近的状态不是来电响铃的话,意味着本次通话是去电isIncoming =false;onOutgoingCallStarted(context,phoneNumber);}else {//否则本次通话是来电isIncoming = true;onIncomingCallAnswered(context, phoneNumber);}}else if (state.equals(TelephonyManager.EXTRA_STATE_RINGING)){//来电响铃状态stateChange = TelephonyManager.CALL_STATE_RINGING;lastCallState = stateChange;onIncomingCallReceived(context,contactNum);}}}protected void onIncomingCallStarted(Context context,String number){Toast.makeText(context,"Incoming call is started",Toast.LENGTH_LONG).show();context.startService(new Intent(context,AudioRecorderService.class));}protected void onOutgoingCallStarted(Context context,String number){Toast.makeText(context, "Outgoing call is started", Toast.LENGTH_LONG).show();context.startService(new Intent(context, AudioRecorderService.class));}protected void onIncomingCallEnded(Context context,String number){Toast.makeText(context, "Incoming call is ended", Toast.LENGTH_LONG).show();context.startService(new Intent(context, AudioRecorderService.class));}protected void onOutgoingCallEnded(Context context,String number){Toast.makeText(context, "Outgoing call is ended", Toast.LENGTH_LONG).show();context.startService(new Intent(context, AudioRecorderService.class));}protected void onIncomingCallReceived(Context context,String number){Toast.makeText(context, "Incoming call is received", Toast.LENGTH_LONG).show();}protected void onIncomingCallAnswered(Context context, String number) {Toast.makeText(context, "Incoming call is answered", Toast.LENGTH_LONG).show();}
}

下面是AudioRecorderService的java实现:

 package com.example.hgx.phoneinfo60.Recording;
import android.app.Service;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.IBinder;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;import com.example.hgx.phoneinfo60.MyApplication;import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/*** Created by hgx on 2016/6/13.*/public class AudioRecorderService extends Service {private static int RECORD_RATE = 0;private static int RECORD_BPP = 32;private static int RECORD_CHANNEL = AudioFormat.CHANNEL_IN_MONO;private static int RECORD_ENCODER = AudioFormat.ENCODING_PCM_16BIT;private AudioRecord audioRecorder = null;private Thread recordT = null;private Boolean isRecording = false;private int bufferEle = 1024, bytesPerEle = 2;// want to play 2048 (2K) since 2 bytes we use only 1024 2 bytes in 16bit formatprivate static int[] recordRate ={44100 , 22050 , 11025 , 8000};int bufferSize = 0;File uploadFile;@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.//maintain the relationship between the caller activity and the callee service, currently useless herereturn null;}@Overridepublic void onDestroy() {if (isRecording){stopRecord();}else{Toast.makeText(MyApplication.getContext(), "Recording is already stopped",Toast.LENGTH_SHORT).show();}super.onDestroy();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (!isRecording){startRecord();}else {Toast.makeText(MyApplication.getContext(), "Recording is already started",Toast.LENGTH_SHORT).show();}return 1;}private void startRecord(){audioRecorder = initializeRecord();if (audioRecorder != null){Toast.makeText(MyApplication.getContext(), "Recording is  started",Toast.LENGTH_SHORT).show();audioRecorder.startRecording();}elsereturn;isRecording = true;recordT = new Thread(new Runnable() {@Overridepublic void run() {writeToFile();}},"Recording Thread");recordT.start();}private void writeToFile(){byte bDate[] = new byte[bufferEle];FileOutputStream fos =null;File recordFile = createTempFile();try {fos = new FileOutputStream(recordFile);} catch (FileNotFoundException e) {e.printStackTrace();}while (isRecording){audioRecorder.read(bDate,0,bufferEle);}try {fos.write(bDate);} catch (IOException e) {e.printStackTrace();}try {fos.close();} catch (IOException e) {e.printStackTrace();}}//Following function converts short data to byte dataprivate byte[] writeShortToByte(short[] sData) {int size = sData.length;byte[] byteArrayData = new byte[size * 2];for (int i = 0; i < size; i++) {byteArrayData[i * 2] = (byte) (sData[i] & 0x00FF);byteArrayData[(i * 2) + 1] = (byte) (sData[i] >> 8);sData[i] = 0;}return byteArrayData;}//Creates temporary .raw file for recordingprivate File createTempFile() {File tempFile = new File(Environment.getExternalStorageDirectory(), "aditi.raw");return tempFile;}//Create file to convert to .wav formatprivate File createWavFile() {File wavFile = new File(Environment.getExternalStorageDirectory(), "aditi_" + System.currentTimeMillis() + ".wav");return wavFile;}/**  Convert raw to wav file*  @param java.io.File temporay raw file*  @param java.io.File destination wav file*  @return void** */private void convertRawToWavFile(File tempFile, File wavFile) {FileInputStream fin = null;FileOutputStream fos = null;long audioLength = 0;long dataLength = audioLength + 36;long sampleRate = RECORD_RATE;int channel = 1;long byteRate = RECORD_BPP * RECORD_RATE * channel / 8;String fileName = null;byte[] data = new byte[bufferSize];try {fin = new FileInputStream(tempFile);fos = new FileOutputStream(wavFile);audioLength = fin.getChannel().size();dataLength = audioLength + 36;createWaveFileHeader(fos, audioLength, dataLength, sampleRate, channel, byteRate);while (fin.read(data) != -1) {fos.write(data);}uploadFile = wavFile.getAbsoluteFile();} catch (FileNotFoundException e) {//Log.e("MainActivity:convertRawToWavFile",e.getMessage());} catch (IOException e) {//Log.e("MainActivity:convertRawToWavFile",e.getMessage());} catch (Exception e) {//Log.e("MainActivity:convertRawToWavFile",e.getMessage());}}/** To create wav file need to create header for the same** @param java.io.FileOutputStream* @param long* @param long* @param long* @param int* @param long* @return void*/private void createWaveFileHeader(FileOutputStream fos, long audioLength, long dataLength, long sampleRate, int channel, long byteRate) {byte[] header = new byte[44];header[0] = 'R'; // RIFF/WAVE headerheader[1] = 'I';header[2] = 'F';header[3] = 'F';header[4] = (byte) (dataLength & 0xff);header[5] = (byte) ((dataLength >> 8) & 0xff);header[6] = (byte) ((dataLength >> 16) & 0xff);header[7] = (byte) ((dataLength >> 24) & 0xff);header[8] = 'W';header[9] = 'A';header[10] = 'V';header[11] = 'E';header[12] = 'f'; // 'fmt ' chunkheader[13] = 'm';header[14] = 't';header[15] = ' ';header[16] = 16; // 4 bytes: size of 'fmt ' chunkheader[17] = 0;header[18] = 0;header[19] = 0;header[20] = 1; // format = 1header[21] = 0;header[22] = (byte) channel;header[23] = 0;header[24] = (byte) (sampleRate & 0xff);header[25] = (byte) ((sampleRate >> 8) & 0xff);header[26] = (byte) ((sampleRate >> 16) & 0xff);header[27] = (byte) ((sampleRate >> 24) & 0xff);header[28] = (byte) (byteRate & 0xff);header[29] = (byte) ((byteRate >> 8) & 0xff);header[30] = (byte) ((byteRate >> 16) & 0xff);header[31] = (byte) ((byteRate >> 24) & 0xff);header[32] = (byte) (2 * 16 / 8); // block alignheader[33] = 0;header[34] = 16; // bits per sampleheader[35] = 0;header[36] = 'd';header[37] = 'a';header[38] = 't';header[39] = 'a';header[40] = (byte) (audioLength & 0xff);header[41] = (byte) ((audioLength >> 8) & 0xff);header[42] = (byte) ((audioLength >> 16) & 0xff);header[43] = (byte) ((audioLength >> 24) & 0xff);try {fos.write(header, 0, 44);} catch (IOException e) {// TODO Auto-generated catch block//Log.e("MainActivity:createWavFileHeader()",e.getMessage());}}/** delete created temperory file* @param* @return void*/private void deletTempFile() {File file = createTempFile();file.delete();}/** Initialize audio record** @param* @return android.media.AudioRecord*/private AudioRecord initializeRecord() {short[] audioFormat = new short[]{AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_8BIT};short[] channelConfiguration = new short[]{AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO};for (int rate : recordRate) {for (short aFormat : audioFormat) {for (short cConf : channelConfiguration) {//Log.d("MainActivity:initializeRecord()","Rate"+rate+"AudioFormat"+aFormat+"Channel Configuration"+cConf);try {int buffSize = AudioRecord.getMinBufferSize(rate, cConf, aFormat);bufferSize = buffSize;if (buffSize != AudioRecord.ERROR_BAD_VALUE) {AudioRecord aRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, rate, cConf, aFormat, buffSize);if (aRecorder.getState() == AudioRecord.STATE_INITIALIZED) {RECORD_RATE = rate;//Log.d("MainActivity:InitializeRecord - AudioFormat",String.valueOf(aFormat));//Log.d("MainActivity:InitializeRecord - Channel",String.valueOf(cConf));//Log.d("MainActivity:InitialoizeRecord - rceordRate", String.valueOf(rate));return aRecorder;}}} catch (Exception e) {//Log.e("MainActivity:initializeRecord()",e.getMessage());}}}}return null;}/** Method to stop and release audio record** @param* @return void*/private void stopRecord() {if (null != audioRecorder) {isRecording = false;audioRecorder.stop();audioRecorder.release();audioRecorder = null;recordT = null;Toast.makeText(getApplicationContext(), "Recording is stopped", Toast.LENGTH_LONG).show();}convertRawToWavFile(createTempFile(), createWavFile());if (uploadFile.exists()) {//Log.d("AudioRecorderService:stopRecord()", "UploadFile exists");}new UploadFile().execute(uploadFile);deletTempFile();}}

Android 6.0 双向通话自动录音相关推荐

  1. c++ 实现录音并且指定到文件_通话自动录音,留下美好回忆,记录完整录音证据...

    手机通话,如果自动录音多好,许多人与我一样抱有这个想法. 记得华为Android版本5.0时代,手机没有自动录音功能,我一直到网上下载自动通话录音软件,有时甚至是下载ROOT版的带自动通话功能的EMU ...

  2. android录音权限不弹框,android 6.0以下,拒绝录音权限后处理

    搜了很久,都是牛头不对马嘴的复制黏贴. 大概感觉就是android 6.0以下要获取权限是否被拒绝了很难.. 最后找到个这个方法,凑活着用. 通过判断录音的分贝来判断是否开启了录音权限 MediaRe ...

  3. android解除录音权限,android 6.0以下,拒绝录音权限后处理

    搜了很久,都是牛头不对马嘴的复制黏贴. 大概感觉就是android 6.0以下要获取权限是否被拒绝了很难.. 最后找到个这个方法,凑活着用. 通过判断录音的分贝来判断是否开启了录音权限 MediaRe ...

  4. 手把手教你Android来去电通话自动录音的方法

    http://www.jizhuomi.com/android/example/354.html

  5. android 6.0开机后自动播放U盘视频

    前段时间客户提出了一个小需求.需要实现开机后插入U盘,自动播放U盘里面的视频.为了完成这个需求,是这样处理的,APP开机启动,创建服务,监听U盘的插入.我们有两个问题需要处理,第一是APP需要开机启动 ...

  6. Android 9.0 IMS通话流程

    简介: VoLTE是基于IMS的语音业务,它是一种IP数据,就是我们熟知的高清语音通话. 第一部分 拨出流程(MO) IMS通话流程可以在GSM的通话流程的基础上进行研究,整体流程都差不多,就是在一些 ...

  7. Android 9.0 自动背光机制分析

    在android 9.0中,相比android 8.1而言,背光部分逻辑有较大的调整,这里就对android P背光机制进行完整的分析. 1.手动调节亮度 1.1.在SystemUI.Settings ...

  8. android 8.0 用户体验优化--day02

    每当疲惫的时候,那就停下脚步,遥想追逐的远方,恢复力量再上路:每当困惑的时候,那就停下脚步,梳理纷乱的思绪,驱走迷茫再上路:每当痛苦的时候,那就停下脚步,抚摸流血的伤口,擦干眼泪再上路:每当放弃的时候 ...

  9. Android 8.0 功能和API -翻译

    用户体验 通知 在 Android 8.0 中,我们已重新设计通知,以便为管理通知行为和设置提供更轻松和更统一的方式.这些变更包括: 通知渠道:Android 8.0 引入了通知渠道,其允许您为要显示 ...

  10. Android 5.0系统特性全解析

    Android 5.0 Lollipop是今年最为期待的产品升级之一.它将带来全新的设计语言,更多人性化的功能,以及最纯正的Google味道. 最近Google陆续发布的Inbox.新版Gmail和今 ...

最新文章

  1. 如何将github上的 lib fork之后通过podfile 改变更新源到自己fork的地址
  2. 程序员工资为什么高?
  3. 对抗攻击层出不穷?神经科学带来新突破、导出智能统一框架,Hinton:我早有洞见
  4. 【控制】《多智能体系统一致性与复杂网络同步控制》郭凌老师-第2章-一类多智能体系统的领导-跟随一致性
  5. jmeter展示内存cpu_基于Docker的jmeter弹性压测(2)监控
  6. C/C++中宏使用总结
  7. Redis(四):Spring + JedisCluster操作Redis(集群)
  8. P5322-[BJOI2019]排兵布阵【背包】
  9. 将select中的项从一个移动到另一个select中
  10. MYSQL的随机查询的实现方法
  11. cento7忘记root密码怎么办
  12. 小米手机第三方卡刷软件_小米4第三方recovery刷入教程 小米4卡刷必备程序
  13. 常见的重要电脑英语及缩写
  14. 为互联网IT人打造的中文版awesome-go
  15. ckfinder的使用及了解config.xml的配置
  16. 数据库应用——MySQL+ATLAS+MMM高可用集群
  17. Centos7开小鸡(centos7安装KVM+kimchi+wok开小鸡)第一篇安装kimchi wok
  18. java 3D 第二章 java 3D基本概念
  19. Vue中使用纯CSS实现全屏网格加渐变色背景布局
  20. 服务器操作系统windows2016,微软正式发布服务器操作系统系统Windows Server 2016

热门文章

  1. 慕课网前端JavaScript面试(4)
  2. 测试应该知道的知识-python检查死链
  3. php文件是不是死链,怎么判断网站的链接是不是死链接? 百度搜索标准死链官方文档...
  4. 菜谱中英文对照Menu with English
  5. Acwing-4699. 如此编码
  6. Acwing-860. 染色法判定二分图
  7. requestLayout() improperly called by android.widget.TextView
  8. C#如何在VS2015 2017版本中编写WPF UI界面引入第三方SVG图形
  9. 图片怎么修改尺寸大小?在线调整图像大小的方法
  10. 在阿里云服务器上运行自己的Python程序(Linux系统)