Android通话录音功能代码实现及问题咨询(含示例代码)
Android通话录音功能完善方案
针对你现有代码的问题,结合Android系统规范和兼容性要求,以下是可实现双方通话录音的完整修正方案:
现有代码核心问题
- 音频源选择错误:使用
MIC仅能录制本地麦克风声音,无法捕获对方通话音频 - 组件生命周期不匹配:
BroadcastReceiver生命周期极短,执行完onReceive后进程可能被回收,导致录音中断 - 文件格式不统一:设置输出格式为
THREE_GPP,但文件后缀用.mp3,导致文件无法正常播放 - 触发时机不合理:在
RINGING(响铃)状态启动录音,此时通话未接通,无实际意义 - 存储适配缺失:Android 10+已废弃
Environment.getExternalStoragePublicDirectory,未适配Scoped Storage规范 - 权限处理不完整:缺少权限请求结果回调,未处理用户拒绝权限的场景
- 资源释放错误:
stopRecording中重复调用reset,未正确释放MediaRecorder资源
一、更新Manifest权限配置
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-feature android:name="android.hardware.telephony" android:required="false" /> <!-- 基础录音权限 --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- Android 9及以下存储权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- 监听电话状态 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- 监听去电 --> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> <!-- Android 12+ 监听电话状态补充权限 --> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.CallRecording" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 监听通话状态的广播接收器 --> <receiver android:name=".CallReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.PHONE_STATE"/> <action android:name="android.intent.action.NEW_OUTGOING_CALL"/> </intent-filter> </receiver> <!-- 后台录音服务(Android 8.0+需前台服务) --> <service android:name=".CallRecordingService" android:foregroundServiceType="mediaProjection|microphone" /> </application> </manifest>
二、用Service替代BroadcastReceiver管理录音
CallReceiver.kt(仅负责触发服务)
class CallReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { context ?: return val action = intent?.action when (action) { // 通话接通状态 TelephonyManager.EXTRA_STATE_OFFHOOK -> startRecordingService(context) // 通话结束状态 TelephonyManager.EXTRA_STATE_IDLE -> stopRecordingService(context) // 发起去电时启动服务 Intent.ACTION_NEW_OUTGOING_CALL -> { val serviceIntent = Intent(context, CallRecordingService::class.java).apply { putExtra("PHONE_NUMBER", intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)) } startForegroundService(context, serviceIntent) } } } private fun startRecordingService(context: Context) { val serviceIntent = Intent(context, CallRecordingService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) } else { context.startService(serviceIntent) } } private fun stopRecordingService(context: Context) { context.stopService(Intent(context, CallRecordingService::class.java)) } }
CallRecordingService.kt(核心录音逻辑)
class CallRecordingService : Service() { private var mediaRecorder: MediaRecorder? = null private var isRecording = false private val NOTIFICATION_ID = 1001 override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() // 启动前台服务,避免被系统回收(Android 8.0+强制要求) startForeground(NOTIFICATION_ID, createRecordingNotification()) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (!isRecording) startRecording() return START_STICKY } override fun onDestroy() { super.onDestroy() stopRecording() } private fun startRecording() { try { mediaRecorder = MediaRecorder().apply { // 适配通话场景的音频源,不同设备可能需调整 setAudioSource( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { MediaRecorder.AudioSource.VOICE_COMMUNICATION } else { MediaRecorder.AudioSource.VOICE_CALL } ) // 统一输出格式与文件后缀 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setOutputFile(getRecordingFilePath()) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(44100) setAudioEncodingBitRate(128000) } mediaRecorder?.prepare() mediaRecorder?.start() isRecording = true } catch (e: IOException) { Log.e("CallRec", "录音启动失败: ${e.message}") stopSelf() } } private fun stopRecording() { if (isRecording) { try { mediaRecorder?.stop() } catch (e: IllegalStateException) { Log.e("CallRec", "录音未启动,无法停止") } finally { mediaRecorder?.reset() mediaRecorder?.release() mediaRecorder = null isRecording = false } } stopForeground(STOP_FOREGROUND_REMOVE) } private fun getRecordingFilePath(): String { val fileName = "Call_${System.currentTimeMillis()}.mp4" return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ 使用MediaStore适配Scoped Storage val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.MIME_TYPE, "audio/mp4") put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_MUSIC}/CallRecordings") } val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues) uri?.let { contentResolver.openFileDescriptor(it, "w")?.use { fd -> return fd.fileDescriptor.toString() } } ?: throw IOException("无法创建录音文件") } else { // Android 9及以下使用传统存储路径 val dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), "CallRecordings") if (!dir.exists()) dir.mkdirs() File(dir, fileName).absolutePath } } private fun createRecordingNotification(): Notification { val channelId = "CallRecordingChannel" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "通话录音", NotificationManager.IMPORTANCE_LOW).apply { setSound(null, null) } (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel) } val launchIntent = packageManager.getLaunchIntentForPackage(packageName) val pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(this, channelId) .setContentTitle("正在录制通话") .setContentText("点击返回应用") .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .setPriority(NotificationCompat.PRIORITY_LOW) .build() } }
三、完善MainActivity权限处理
class MainActivity : AppCompatActivity() { private val PERMISSION_REQUEST_CODE = 123 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val makeCall: Button = findViewById(R.id.makeCall) makeCall.setOnClickListener { if (hasPermissions()) { startActivity(Intent(Intent.ACTION_DIAL)) } else { requestPermissions() } } if (!hasPermissions()) requestPermissions() } private fun hasPermissions(): Boolean { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS ) } else { arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE ) } return permissions.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED } } private fun requestPermissions() { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS ) } else { arrayOf( Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE ) } ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PERMISSION_REQUEST_CODE) { val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } if (!allGranted) { Toast.makeText(this, "需授予所有权限才能使用通话录音功能", Toast.LENGTH_LONG).show() } } } }
关键兼容性说明
- 音频源适配:不同厂商设备对音频源支持不同,
VOICE_COMMUNICATION是官方推荐的通话场景源,若无法录制对方声音,可尝试VOICE_CALL(部分Android 10+设备可能限制此权限) - 系统限制:小米、华为等厂商对通话录音有系统级限制,普通APP可能无法直接录制双方声音,需引导用户开启系统自带录音功能,或通过无障碍服务实现(需用户手动授权)
- 前台服务:Android 8.0+必须将录音服务设为前台服务,否则会被系统强制停止
- 存储规范:严格遵循Scoped Storage要求,避免使用已废弃的存储API
内容的提问来源于stack exchange,提问作者Hrithik Rajput
相关产品推荐
相关产品推荐

