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.

4.8 (4) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

Permission prompt starting with: Start recording or casting with Cat Sounds?

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:

  1. First, you check whether the app requested the correct permission, which is the one you added in your companion object.
  2. Next, you check if the result was OK, meaning the user clicked on the START NOW button.
  3. If they clicked the START NOW button, you’ll show a toast saying the cast will start.
  4. 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:

  1. 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.
  2. Then you start the service in the foreground. Starting audio capturing in the background may cause trouble if Android kills your app.
  3. 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.

Start Audio Capture Toast

And the other if you choose to stop the audio capture.

Stop Audio Capture Toast

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:

  1. The service checks whether it received an intent to start action.
  2. You initialize mediaProjection to store the information from the audio capture.
  3. You call the method that will do the audio capture.
  4. This is the item you need to return from onStartCommand, which means the service will stay running until something triggers the stop command.
  5. When you trigger onStartCommand with the stop action, you execute the stop capture method.
  6. Then you start the service, but this time, with the non-sticky statement because the service doesn’t need to keep running.
  7. If the action is neither start nor stop, you throw an exception because that’s not expected.
  8. 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.