background

Recently, I need to do such a thing, a service to complete the recording function of multiple apps, roughly the following logic

  • Services are integrated into each end in the form of lib
  • When the main App exists, all other apps use the recording service of the main App
  • If the main App does not exist, other apps use their own recording service
  • The App with the highest priority has the absolute right to record, pauses regardless of whether other apps are recording, and prioritizes requests from the App with the highest priority
  • Support AudioRecord, MediaRecorder two recording solutions

Why do we do this?

  • The Bottom layer of The Android system is limited to recording. Only one process can use the recording function at a time
  • Business needs, all transactions to ensure the recording function of the main App
  • In order to better manage the recording status, as well as the problem of communication between multiple apps

Architecture diagram design

The App layer

This includes all terminals that need integrated recording services. No explanation is required here

The Manager layer

This layer is responsible for the management of Service layer, including: Service binding, unbinding, registration callback, start recording, stop recording, check recording status, check Service running status and so on

The Service layer

The core logic layer, through the implementation of AIDL, satisfies the cross-process communication and provides the actual recording function.

Directory in

IRecorder interface definition

public interface IRecorder {

    String startRecording(RecorderConfig recorderConfig);

    void stopRecording();

    RecorderState state();

    boolean isRecording();

}
Copy the code

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 = nullreturn "MediaRecorder does not support AudioForm.pcm"} } } catch (e: IllegalStateException) { mMediaRecorder? .reset() mMediaRecorder? .release() mMediaRecorder = nullreturn "Error initializing media Recorder initialization failed";
        }
        returntry { 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
    }

}
Copy the code

The important thing to note here is to add @synchronized because it is safe to add @synchronized when multiple processes are called at the same time.

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);

}

Copy the code

Note: custom parameters are required to implement Parcelable interfaces and callbacks are required to AIDL interface definitions

AIDL Interface callback definition

interface IRecorderCallBack {

    void onStart(in RecorderResult result);

    void onStop(in RecorderResult result);

    void onException(String error,in RecorderResult result);

}
Copy the code

RecorderService implementation

Next comes the core of functionality, cross-process services

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? {
            returncurrentRecorderResult } 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) {// stop the currentWeight as long as the weight is greater than the 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 {
        returniRecorder? .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()
    }

}
Copy the code

A few things to note here: Because it is a cross-process service, it is possible for multiple apps to start recording at the same time, and it is possible for one app to record while another app calls the stop function, so maintain the currentRecorderResult object. Also important is the currentWeight field, which is primarily a matter of maintaining priority, as long as there is an instruction higher than the current priority, the recording service will operate according to the new instruction. NotifyCallBack calls the AIDL callback when appropriate, notifying the App to do the corresponding operation.

RecorderManager implementation

Step 1 Service registration. Here, start by the package name of the main App. All apps start in this way

fun initialize(context: Context? , serviceConnectState: ((Boolean) -> Unit)? = null) { mApplicationContext = context? .applicationContextif(! 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()
           }
       }
   }
Copy the code

If isCanBind is false, that is, if the main App is not found, then 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")}Copy the code

Step 2 After the connection is successful

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

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

You can then use the mRecorderService to manipulate the AIDL interface, eventually calling the implementation of the RecorderService

// Start fun startRecording(recorderConfig: recorderConfig? {if(recorderConfig ! = null) mRecorderService? .startrecording (recorderConfig)} // stopRecording(recorderConfig: recorderConfig?) {if(recorderConfig ! = null) mRecorderService? .stoprecording (recorderConfig)} fun isRecording(recorderConfig: recorderConfig?) : Boolean {returnmRecorderService? .isRecording(recorderConfig) ? :false
    }
Copy the code

Such a complete set of cross-process communication is complete, code comments are few, through the process of code display, should be able to understand the overall call flow. If you don’t understand, welcome to the comment area.

conclusion

Through these two days, I have a deeper understanding of the cross-process data processing of the recording service implemented by AIDL. There are several difficult things to deal with, namely the maintenance of the recording state and the maintenance of the priority. It is also very easy to deal with these two points. Cut it out. There’s a problem.

Welcome to exchange: Git source code