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