You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android桌面Widget录音功能异常求助:按钮失效/无音频输出

Android桌面组件(Widget)录音功能问题排查与解决

方案一:RecordService实现按钮无响应问题

问题描述

点击Widget按钮无法触发录音,预期点击后启动Service并5秒后自动停止录音,但按钮无任何响应。

问题根源

  1. 后台Service限制:Android 8.0+对后台Service启动有严格限制,直接通过PendingIntent.getService启动的Service会被系统拦截,必须使用前台Service。
  2. PendingIntent标识问题:FLAG_IMMUTABLE在部分场景下会导致Intent无法正确传递,且相同requestCode的PendingIntent会被复用,可能导致Action不生效。
  3. 未处理权限异常:缺少录音权限或未处理MediaRecorder初始化时的异常,导致录音流程静默失败。
  4. Handler延迟任务不可靠:Service被系统回收时,主线程Handler的延迟任务会被取消,无法触发自动停止逻辑。

修复方案

1. 配置权限与Service声明

在AndroidManifest.xml中添加录音权限,并将Service声明为前台服务:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<service
    android:name=".RecordService"
    android:foregroundServiceType="mediaRecording" />

2. 修改RecordService为前台服务,并优化录音逻辑

class RecordService : Service() {

    companion object {
        const val START_RECORDING_ACTION = "START_RECORDING"
        private const val NOTIFICATION_ID = 1001
        private const val CHANNEL_ID = "RECORD_SERVICE_CHANNEL"
    }

    private var recorder: MediaRecorder? = null
    private var coroutineJob: Job? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            START_RECORDING_ACTION -> startRecording()
        }
        return START_STICKY
    }

    private fun startRecording() {
        if (recorder != null) return

        // 创建前台通知,避免Service被系统杀死
        createNotificationChannel()
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("正在录音")
            .setContentText("点击停止")
            .setSmallIcon(R.drawable.ic_mic)
            .build()
        startForeground(NOTIFICATION_ID, notification)

        try {
            recorder = MediaRecorder().apply {
                setAudioSource(MediaRecorder.AudioSource.MIC)
                setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                setOutputFile(getOutputFile().absolutePath)
                prepare()
                start()
            }
            Log.d("RECORD", "录音已启动")

            // 用Coroutine替代Handler,更可靠
            coroutineJob = CoroutineScope(Dispatchers.IO).launch {
                delay(5000)
                stopRecording()
            }
        } catch (e: Exception) {
            Log.e("RECORD", "录音初始化失败: ${e.message}")
            stopSelf()
        }
    }

    private fun stopRecording() {
        recorder?.run {
            try {
                stop()
                release()
            } catch (e: Exception) {
                Log.e("RECORD", "停止录音失败: ${e.message}")
            }
        }
        recorder = null
        coroutineJob?.cancel()
        stopForeground(STOP_FOREGROUND_REMOVE)
        stopSelf()
        Log.d("RECORD", "录音已停止")
    }

    private fun getOutputFile(): File {
        val fileName = "recording_${System.currentTimeMillis()}.mp4"
        return File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), fileName)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "录音服务",
                NotificationManager.IMPORTANCE_LOW
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        stopRecording()
    }

    override fun onBind(intent: Intent): IBinder? = null
}

3. 修改Widget的PendingIntent获取逻辑

class RecordWidget : AppWidgetProvider() {

    @SuppressLint("RemoteViewLayout")
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        appWidgetIds.forEach { appWidgetId ->
            val views = RemoteViews(context.packageName, R.layout.widget_layout)
            views.setOnClickPendingIntent(
                R.id.record_button,
                getRecordPendingIntent(context)
            )
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }

    private fun getRecordPendingIntent(context: Context): PendingIntent {
        val intent = Intent(context, RecordService::class.java).apply {
            action = RecordService.START_RECORDING_ACTION
        }
        // 使用FLAG_MUTABLE适配Android 12+,并设置唯一requestCode避免PendingIntent复用
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            PendingIntent.getForegroundService(
                context,
                System.currentTimeMillis().toInt(),
                intent,
                PendingIntent.FLAG_MUTABLE
            )
        } else {
            PendingIntent.getService(
                context,
                System.currentTimeMillis().toInt(),
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        }
    }
}

方案二:Broadcast实现无音频文件问题

问题描述

点击启停按钮能弹出提示,但生成的文件无音频内容,文件可能损坏或为空。

问题根源

  1. 文件格式不匹配:输出格式设置为THREE_GPP,但文件名后缀为.mp3,格式不兼容导致文件无法正常写入音频数据。
  2. BroadcastReceiver生命周期限制:AppWidgetProvider是BroadcastReceiver子类,每次广播触发都会创建新实例,成员变量mediaRecorder在停止录音时无法获取到之前的实例,导致录音未正确停止。
  3. 未处理异常:MediaRecorder的prepare、start、stop操作未捕获异常,静默失败后无法生成有效音频文件。

修复方案

1. 统一文件格式

将输出格式与文件名后缀对应,比如使用MPEG_4格式+.mp4后缀:

mediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
val file = File(musicDirectory, "recording_${System.currentTimeMillis()}.mp4").path

2. 使用单例托管MediaRecorder实例

创建单例类管理MediaRecorder,避免BroadcastReceiver实例销毁导致引用丢失:

object RecorderManager {
    private var mediaRecorder: MediaRecorder? = null

    fun startRecording(context: Context): Boolean {
        return try {
            mediaRecorder = MediaRecorder().apply {
                setAudioSource(MediaRecorder.AudioSource.MIC)
                setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                val outputFile = File(
                    context.getExternalFilesDir(Environment.DIRECTORY_MUSIC),
                    "recording_${System.currentTimeMillis()}.mp4"
                ).absolutePath
                setOutputFile(outputFile)
                prepare()
                start()
            }
            true
        } catch (e: Exception) {
            e.printStackTrace()
            false
        }
    }

    fun stopRecording() {
        mediaRecorder?.run {
            try {
                stop()
                release()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        mediaRecorder = null
    }
}

3. 修改Widget的广播处理逻辑

class RecordWidget : AppWidgetProvider() {

    @SuppressLint("RemoteViewLayout")
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        appWidgetIds.forEach { widgetId ->
            updateAppWidget(context, appWidgetManager, widgetId)
        }
    }

    private fun pendingIntent(context: Context, action: String): PendingIntent {
        val intent = Intent(context, javaClass).apply {
            this.action = action
        }
        return PendingIntent.getBroadcast(
            context,
            action.hashCode(), // 不同action使用不同requestCode避免复用
            intent,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
            } else {
                PendingIntent.FLAG_UPDATE_CURRENT
            }
        )
    }

    private fun updateAppWidget(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int
    ) {
        val views = RemoteViews(context.packageName, R.layout.widget_layout)
        views.setOnClickPendingIntent(R.id.record_button, pendingIntent(context, "START_RECORD"))
        views.setOnClickPendingIntent(R.id.stopBtn, pendingIntent(context, "STOP_RECORD"))
        appWidgetManager.updateAppWidget(appWidgetId, views)
    }

    override fun onReceive(context: Context?, intent: Intent?) {
        super.onReceive(context, intent)
        context ?: return
        val action = intent?.action ?: return

        when (action) {
            "START_RECORD" -> {
                val success = RecorderManager.startRecording(context)
                Toast.makeText(
                    context,
                    if (success) "录音已启动" else "录音启动失败",
                    Toast.LENGTH_SHORT
                ).show()
            }
            "STOP_RECORD" -> {
                RecorderManager.stopRecording()
                Toast.makeText(context, "录音已停止", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者yashveer singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 10:46:21