Android perfect cross-process service sharing practice

background

Recently, such a thing needs to be done. A service to complete the recording function of multiple apps, roughly has the following logic

  • The service is integrated into each end in the form of lib
  • When the main app exists, all other apps use the main app's recording service
  • When the main app does not exist, other apps use their own recording service
  • There is priority, apps with high priority have absolute recording permissions, no matter whether other apps are recording, they must be paused, and priority is given to requests for high-priority apps.
  • Supports two recording solutions: AudioRecord and MediaRecorder

Why is it so designed?

  • The bottom layer of the Android system has restrictions on recording, and only one process can use the recording function at a time
  • Business needs, all transactions guarantee the recording function of the main App
  • In order to better manage the recording status, and the communication between multiple apps

Architecture diagram design

Architecture

App layer

Contains all the company's need to integrate recording services, no explanation is needed here

Manager layer

This layer is responsible for the management of the Service layer, including: service binding, unbinding, registration callback, start recording, stop recording, check recording status, check service running status, etc. ### Service layer core logic layer, through the realization of AIDL, To meet cross-process communication, and provide the actual recording function.

Directory at a glance

Looking at the distribution of the code directory, combined with the architecture diagram, we come to implement a set of logic from the bottom to the upper layer

IRecorder interface definition

public interface IRecorder {

    String startRecording(RecorderConfig recorderConfig);

    void stopRecording();

    RecorderState state();

    boolean isRecording();
}

IRecorder interface implementation

class JLMediaRecorder : IRecorder {

    private var mMediaRecorder: MediaRecorder? = null
    private var mState = RecorderState.IDLE

    @Synchronized
    override fun startRecording(recorderConfig: RecorderConfig): String {
        try {
            mMediaRecorder = MediaRecorder()
            mMediaRecorder?.setAudioSource(recorderConfig.audioSource)

            when (recorderConfig.recorderOutFormat) {
                RecorderOutFormat.MPEG_4 -> {
                    mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                    mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                }
                RecorderOutFormat.AMR_WB -> {
                    mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB)
                    mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB)
                }
                else -> {
                    mMediaRecorder?.reset()
                    mMediaRecorder?.release()
                    mMediaRecorder = null
                    return "MediaRecorder 不支持 AudioFormat.PCM"
                }
            }
        } catch (e: IllegalStateException) {
            mMediaRecorder?.reset()
            mMediaRecorder?.release()
            mMediaRecorder = null
            return "Error initializing media recorder 初始化失败";
        }
        return try {
            val file = recorderConfig.recorderFile
            file.parentFile.mkdirs()
            file.createNewFile()
            val outputPath: String = file.absolutePath

            mMediaRecorder?.setOutputFile(outputPath)
            mMediaRecorder?.prepare()
            mMediaRecorder?.start()
            mState = RecorderState.RECORDING
            ""
        } catch (e: Exception) {
            mMediaRecorder?.reset()
            mMediaRecorder?.release()
            mMediaRecorder = null
            recorderConfig.recorderFile.delete()
            e.toString()
        }
    }

    override fun isRecording(): Boolean {
        return mState == RecorderState.RECORDING
    }

    @Synchronized
    override fun stopRecording() {
        try {
            if (mState == RecorderState.RECORDING) {
                mMediaRecorder?.stop()
                mMediaRecorder?.reset()
                mMediaRecorder?.release()
            }
        } catch (e: java.lang.IllegalStateException) {
            e.printStackTrace()
        }
        mMediaRecorder = null
        mState = RecorderState.IDLE
    }

    override fun state(): RecorderState {
        return mState
    }

}

It is important to note here that  @Synchronizedbecause of the disorder of the state when multiple processes are called at the same time, it is safe to add it.

AIDL interface definition

interface IRecorderService {

    void startRecording(in RecorderConfig recorderConfig);

    void stopRecording(in RecorderConfig recorderConfig);

    boolean isRecording(in RecorderConfig recorderConfig);

    RecorderResult getActiveRecording();

    void registerCallback(IRecorderCallBack callBack);

    void unregisterCallback(IRecorderCallBack callBack);

}

important point:

  • Custom parameters need to implement the Parcelable interface
  • If you need to call back, it is also the AIDL interface definition

AIDL interface callback definition

interface IRecorderCallBack {

    void onStart(in RecorderResult result);

    void onStop(in RecorderResult result);

    void onException(String error,in RecorderResult result);

}

RecorderService implementation

Next is the core of the function, cross-process service

class RecorderService : Service() {

    private var iRecorder: IRecorder? = null
    private var currentRecorderResult: RecorderResult = RecorderResult()
    private var currentWeight: Int = -1

    private val remoteCallbackList: RemoteCallbackList<IRecorderCallBack> = RemoteCallbackList()

    private val mBinder: IRecorderService.Stub = object : IRecorderService.Stub() {

        override fun startRecording(recorderConfig: RecorderConfig) {
            startRecordingInternal(recorderConfig)
        }

        override fun stopRecording(recorderConfig: RecorderConfig) {
            if (recorderConfig.recorderId == currentRecorderResult.recorderId)
                stopRecordingInternal()
            else {
                notifyCallBack {
                    it.onException(
                        "Cannot stop the current recording because the recorderId is not the same as the current recording",
                        currentRecorderResult
                    )
                }
            }
        }

        override fun getActiveRecording(): RecorderResult? {
            return currentRecorderResult
        }

        override fun isRecording(recorderConfig: RecorderConfig?): Boolean {
            return if (recorderConfig?.recorderId == currentRecorderResult.recorderId)
                iRecorder?.isRecording ?: false
            else false
        }

        override fun registerCallback(callBack: IRecorderCallBack) {
            remoteCallbackList.register(callBack)
        }

        override fun unregisterCallback(callBack: IRecorderCallBack) {
            remoteCallbackList.unregister(callBack)
        }

    }

    override fun onBind(intent: Intent?): IBinder? {
        return mBinder
    }


    @Synchronized
    private fun startRecordingInternal(recorderConfig: RecorderConfig) {

        val willStartRecorderResult =
            RecorderResultBuilder.aRecorderResult().withRecorderFile(recorderConfig.recorderFile)
                .withRecorderId(recorderConfig.recorderId).build()

        if (ContextCompat.checkSelfPermission(
                this@RecorderService,
                android.Manifest.permission.RECORD_AUDIO
            )
            != PackageManager.PERMISSION_GRANTED
        ) {
            logD("Record audio permission not granted, can't record")
            notifyCallBack {
                it.onException(
                    "Record audio permission not granted, can't record",
                    willStartRecorderResult
                )
            }
            return
        }

        if (ContextCompat.checkSelfPermission(
                this@RecorderService,
                android.Manifest.permission.WRITE_EXTERNAL_STORAGE
            )
            != PackageManager.PERMISSION_GRANTED
        ) {
            logD("External storage permission not granted, can't save recorded")
            notifyCallBack {
                it.onException(
                    "External storage permission not granted, can't save recorded",
                    willStartRecorderResult
                )
            }
            return
        }

        if (isRecording()) {

            val weight = recorderConfig.weight

            if (weight < currentWeight) {
                logD("Recording with weight greater than in recording")
                notifyCallBack {
                    it.onException(
                        "Recording with weight greater than in recording",
                        willStartRecorderResult
                    )
                }
                return
            }

            if (weight > currentWeight) {
                //只要权重大于当前权重,立即停止当前。
                stopRecordingInternal()
            }

            if (weight == currentWeight) {
                if (recorderConfig.recorderId == currentRecorderResult.recorderId) {
                    notifyCallBack {
                        it.onException(
                            "The same recording cannot be started repeatedly",
                            willStartRecorderResult
                        )
                    }
                    return
                } else {
                    stopRecordingInternal()
                }
            }

            startRecorder(recorderConfig, willStartRecorderResult)

        } else {

            startRecorder(recorderConfig, willStartRecorderResult)

        }

    }

    private fun startRecorder(
        recorderConfig: RecorderConfig,
        willStartRecorderResult: RecorderResult
    ) {
        logD("startRecording result ${willStartRecorderResult.toString()}")

        iRecorder = when (recorderConfig.recorderOutFormat) {
            RecorderOutFormat.MPEG_4, RecorderOutFormat.AMR_WB -> {
                JLMediaRecorder()
            }
            RecorderOutFormat.PCM -> {
                JLAudioRecorder()
            }
        }

        val result = iRecorder?.startRecording(recorderConfig)

        if (!result.isNullOrEmpty()) {
            logD("startRecording result $result")
            notifyCallBack {
                it.onException(result, willStartRecorderResult)
            }
        } else {
            currentWeight = recorderConfig.weight
            notifyCallBack {
                it.onStart(willStartRecorderResult)
            }
            currentRecorderResult = willStartRecorderResult
        }
    }

    private fun isRecording(): Boolean {
        return iRecorder?.isRecording ?: false
    }

    @Synchronized
    private fun stopRecordingInternal() {
        logD("stopRecordingInternal")
        iRecorder?.stopRecording()
        currentWeight = -1
        iRecorder = null
        MediaScannerConnection.scanFile(
            this,
            arrayOf(currentRecorderResult.recorderFile?.absolutePath),
            null,
            null
        )
        notifyCallBack {
            it.onStop(currentRecorderResult)
        }
    }

    private fun notifyCallBack(done: (IRecorderCallBack) -> Unit) {
        val size = remoteCallbackList.beginBroadcast()
        logD("recorded notifyCallBack  size $size")
        (0 until size).forEach {
            done(remoteCallbackList.getBroadcastItem(it))
        }
        remoteCallbackList.finishBroadcast()
    }

}

Points to note here:

Because it is a cross-process service, when starting recording, it may be that multiple apps are started at the same time, or it may be that while one app is recording, another app calls the stop function, so here is the maintenance of the current currentRecorderResultobject A currentWeightfield is also very important. This field is mainly a matter of maintaining priority. As long as there is an instruction higher than the current priority, the recording service will be operated according to the new instruction. notifyCallBack Call the AIDL callback at the appropriate time to notify the App to do the corresponding operation.

RecorderManager implementation

step 1

Service registration, here starts according to the package name of the main App, all apps are started in this way

fun initialize(context: Context?, serviceConnectState: ((Boolean) -> Unit)? = null) {
       mApplicationContext = context?.applicationContext
       if (!isServiceConnected) {
           this.mServiceConnectState = serviceConnectState
           val serviceIntent = Intent()
           serviceIntent.`package` = "com.julive.recorder"
           serviceIntent.action = "com.julive.audio.service"
           val isCanBind = mApplicationContext?.bindService(
               serviceIntent,
               mConnection,
               Context.BIND_AUTO_CREATE
           ) ?: false
           if (!isCanBind) {
               logE("isCanBind:$isCanBind")
               this.mServiceConnectState?.invoke(false)
               bindSelfService()
           }
       }
   }

isCanBind Yes false, that is, the main app is not found, this time you need to start your own service

 private fun bindSelfService() {
        val serviceIntent = Intent(mApplicationContext, RecorderService::class.java)
        val isSelfBind =
            mApplicationContext?.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)
        logE("isSelfBind:$isSelfBind")
    }
  • step 2

After successful connection

  private val mConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            mRecorderService = IRecorderService.Stub.asInterface(service)
            mRecorderService?.asBinder()?.linkToDeath(deathRecipient, 0)
            isServiceConnected = true
            mServiceConnectState?.invoke(true)
        }

        override fun onServiceDisconnected(name: ComponentName) {
            isServiceConnected = false
            mRecorderService = null
            logE("onServiceDisconnected:name=$name")
        }
    }

Then you can use mRecorderService to operate AIDL interfaces, the final call RecorderServiceto achieve

//启动
fun startRecording(recorderConfig: RecorderConfig?) {
        if (recorderConfig != null)
            mRecorderService?.startRecording(recorderConfig)
    }
//暂停
    fun stopRecording(recorderConfig: RecorderConfig?) {
        if (recorderConfig != null)
            mRecorderService?.stopRecording(recorderConfig)
    }
//是否录音中
    fun isRecording(recorderConfig: RecorderConfig?): Boolean {
        return mRecorderService?.isRecording(recorderConfig) ?: false
    }

Such a complete set of cross-process communication is completed, and there are few code comments. After the code display of this process, you should be able to understand the overall call process. If you do not understand, please feel free to leave a message.

to sum up

Through the past two days, I have a deeper understanding of the cross-process data processing of the AIDL recording service. There are several difficult to deal with in the state maintenance of the recording, and the maintenance of priority. It is actually easy to handle these two points. No more, there is a question message area to communicate.

Welcome to exchange: git source code: https://github.com/ibaozi-cn/Multi-Process-Audio-Recorder

Byte Beat Android Interview Question Set (including answer analysis)

Published 488 original articles · praised 85 · 230,000 views +

Guess you like

Origin blog.csdn.net/Coo123_/article/details/104902586