Audio Playback Capture in Android X
Learn how to integrate the Android Playback Capture API into your app, allowing you to record and play back audio from other apps. By Evana Margain Puig.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Audio Playback Capture in Android X
30 mins
- Getting Started
- What Is Audio Playback Capture API?
- Managing Permissions for Audio Capture
- Requesting Permissions
- Checking for Permissions in Your Activity
- Changing Permissions
- Recording Audio From Other Apps
- Requesting Permission Before a Capturing Session
- Checking Permissions
- Overriding onActivityResult
- Creating MediaCaptureService
- Starting the Service and Getting Notifications
- Triggering the Audio Capture Playback
- Starting the Playback Audio Capture
- Stopping the Audio Capture
- Connecting the Service
- Adding Your Service to the Android Manifest
- Disabling Audio Playback Capture
- Listening to Recorded Audio
- Getting the Files From Memory
- Listening to Your Files
- Where to Go From Here?
Checking Permissions
You don’t have the functions for checking permissions yet, so add them below startCapturing()
.
private fun isRecordAudioPermissionGranted(): Boolean {
return ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
private fun requestRecordAudioPermission() {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.RECORD_AUDIO),
RECORD_AUDIO_PERMISSION_REQUEST_CODE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(
requireContext(),
"Permissions to capture audio granted.",
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
requireContext(), "Permissions to capture audio denied.",
Toast.LENGTH_SHORT
).show()
}
}
}
After adding those functions to the fragment, you’ll get some missing import errors. Resolve these by adding:
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import android.content.Context
import android.media.projection.MediaProjectionManager
import androidx.core.content.ContextCompat
Finally, you’ll get an error on RECORD_AUDIO_PERMISSION_REQUEST_CODE
, which tells you that you don’t have a variable or constant with that name. Go to your companion object at the bottom of the class and add the constant:
private const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 42
Build and run. Now, click the START AUDIO CAPTURE button and you’ll see a permission prompt:
Click START NOW now, and nothing happens. That’s because you need to override onActivityResult
. You’ll do that next.
Overriding onActivityResult
As mentioned above, once this alert appears, an activity result will return. onActivityResult()
is a method that’s part of your fragment, but you need to override it to start the cast once the user gives permission.
To do this, add the following code after startMediaProjectionRequest()
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// 1
if (requestCode == MEDIA_PROJECTION_REQUEST_CODE) {
// 2
if (resultCode == Activity.RESULT_OK) {
// 3
Toast.makeText(
requireContext(),
"MediaProjection permission obtained. Foreground service will start to capture audio.",
Toast.LENGTH_SHORT
).show()
}
} else {
// 4
Toast.makeText(
requireContext(), "Request to get MediaProjection denied.",
Toast.LENGTH_SHORT
).show()
}
}
This may look like a big function, but don’t worry. It’s just a series of validations that check whether the user started or canceled the cast. Look at it in greater detail:
- First, you check whether the app requested the correct permission, which is the one you added in your companion object.
- Next, you check if the result was OK, meaning the user clicked on the START NOW button.
- If they clicked the START NOW button, you’ll show a toast saying the cast will start.
- Otherwise, you show another toast saying the user denied the permission.
if you get an onActivity Result Overrides nothing error, make sure that you imported Intent
and Activity
properly by adding the code below to imports:
import android.content.Intent
import android.app.Activity
Permissions are a major part of using such APIs. Now that you have taken care of that, you are going to implement the Audio Capture API itself.
Creating MediaCaptureService
In the following section, you’ll create an Android Service that takes care of the Audio Capture.
You don’t need to know what an Android Service is for this tutorial, but if you’re curious, take a look at the Android Services tutorial.
In the Android View of the project, go to com.raywenderlich.android.cat_audio/services/MediaCaptureService. This file, and the code that you’ll add next, will be the same for almost any project you create. Some code has been pre-populated because the service is a really long class.
Starting the Service and Getting Notifications
Inside the service’s onCreate
, you’ll find a TODO to start the service and notifications. Add the code below to it:
// 1
createNotificationChannel()
// 2
startForeground(SERVICE_ID, NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).build())
// 3
mediaProjectionManager = applicationContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
Here’s a breakdown of the code to understand what’s going on:
- First you create a notification channel. This is one of the prepopulated functions. If you don’t know what a notification channel is, you can learn more about it in this Notifications Tutorial.
- Then you start the service in the foreground. Starting audio capturing in the background may cause trouble if Android kills your app.
- Finally, you get the media projection service that the system provides, which will help capture the audio.
After adding that function, you may, again, see a couple of errors due to missing imports. Add the following to the top of the file:
import android.content.Context
import androidx.core.app.NotificationCompat
import android.media.projection.MediaProjectionManager
Build and run. You’ll now see toasts, one for when you click START NOW in the dialog that asks you whether you want to start the casting.
And the other if you choose to stop the audio capture.
Triggering the Audio Capture Playback
When you start a service, it executes onStartCommand
. In this case, you need to override that method to provide what to do when the service starts.
Replace the return statement inside onStartCommand
with:
// 1
return if (intent != null) {
when (intent.action) {
ACTION_START -> {
// 2
mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, intent.getParcelableExtra(EXTRA_RESULT_DATA)!!) as MediaProjection
// 3
startAudioCapture()
// 4
Service.START_STICKY
}
// 5
ACTION_STOP -> {
stopAudioCapture()
// 6
Service.START_NOT_STICKY
}
// 7
else -> throw IllegalArgumentException("Unexpected action received: ${intent.action}")
}
} else {
// 8
Service.START_NOT_STICKY
}
Now, break down the code above:
- The service checks whether it received an intent to start action.
- You initialize
mediaProjection
to store the information from the audio capture. - You call the method that will do the audio capture.
- This is the item you need to return from
onStartCommand
, which means the service will stay running until something triggers the stop command. - When you trigger
onStartCommand
with the stop action, you execute the stop capture method. - Then you start the service, but this time, with the non-sticky statement because the service doesn’t need to keep running.
- If the action is neither start nor stop, you throw an exception because that’s not expected.
- Finally, if there is no intent, you also start the service with the non-sticky flag, so it will stop after
onStartCommand
finishes.
When you build and run the app at this point, you’ll see no change. That’s because even though you created the service, record fragment isn’t using it yet.
You have two more functions to implement in your service before it works correctly.