Following the previous chapters, this chapter will take the existing VideoPlayer project and enhance it to add Picture-in-Picture support, including detecting PiP mode changes and adding a PiP action designed to display information about the currently running video.
Adding Picture-in-Picture Support to the Manifest
The first step in adding PiP support to an Android app project is to enable it within the project Manifest file. Open the manifests -> AndroidManifest.xml file and modify the activity element to enable PiP support:
.
.
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
.
.
Code language: HTML, XML (xml)
Adding a Picture-in-Picture Button
As currently designed, the layout for the VideoPlayer activity consists solely of a VideoView instance. As currently designed, the layout for the VideoPlayer activity consists solely of a VideoView instance. A button will now be added to the layout to switch to PiP mode. Load the activity_main.xml file into the layout editor and drag a Button object from the palette onto the layout so that it is positioned as shown in Figure 74-1:
Change the text on the button to read “Enter PiP Mode” and extract the string to a resource named enter_pip_ mode. Before moving on to the next step, change the ID of the button to pipButton and configure the onClick attribute to call a method named enterPipMode.
Entering Picture-in-Picture Mode
The enterPipMode onClick callback method must now be added to the MainActivity.kt class file. Locate this file, open it in the code editor, and add this method as follows:
.
.
import android.app.PictureInPictureParams
import android.util.Rational
import android.view.View
import android.content.res.Configuration
.
.
fun enterPipMode(view: View) {
val rational = Rational(binding.videoView1.width,
binding.videoView1.height)
val params = PictureInPictureParams.Builder()
.setAspectRatio(rational)
.build()
binding.pipButton.visibility = View.INVISIBLE
binding.videoView1.setMediaController(null)
enterPictureInPictureMode(params)
}
Code language: Kotlin (kotlin)
The method begins by obtaining a reference to the Button view, then creates a Rational object containing the width and height of the VideoView. A set of Picture-in-Picture parameters is then created using the PictureInPictureParams Builder, passing through the Rational object as the aspect ratio for the video playback. Since the button does not need to be visible while the video is in PiP mode, it is invisible. The video playback controls are also hidden, so the video view will be unobstructed while in PiP mode.
Compile and run the app on a device or emulator running Android version 8 or newer and wait for video playback to begin before clicking on the PiP mode button. The video playback should minimize and appear in the PiP window as shown in Figure 74-2:
Click in the PiP window, then click within the full-screen mode markers that appear in the center of the window. Although the activity returns to full-screen mode, the button and media playback controls remain hidden. Clearly, some code must be added to the project to detect when PiP mode changes occur within the activity.
Detecting Picture-in-Picture Mode Changes
As discussed in the previous chapter, PiP mode changes are detected by overriding the onPictureInPictureModeChanged() method within the affected activity. n this case, the method must be written to detect whether the activity is entering or exiting PiP mode and to take appropriate action to re-activate the PiP button and the playback controls. Remaining within the MainActivity.kt file, add this method now:
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
} else {
binding.pipButton.visibility = View.VISIBLE
binding.videoView1.setMediaController(mediaController)
}
}
Code language: Kotlin (kotlin)
When the method is called, it is passed a Boolean value indicating whether the activity is now in PiP mode. The code in the above method checks this value to decide whether to show the PiP button and to re-activate the playback controls.
Adding a Broadcast Receiver
The final step in the project is to add an action to the PiP window. The purpose of this action is to display a Toast message containing the name of the currently playing video. This will require some communication between the PiP window and the activity. One of the simplest ways to achieve this is to implement a broadcast receiver within the activity and use a pending intent to broadcast a message from the PiP window to the activity. Each time the activity enters PiP mode, these steps must be performed, so code must be added to the onPictureInPictureModeChanged() method. Locate this method now and begin by adding some code to create an intent filter and initialize the broadcast receiver:
.
.
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.widget.Toast
.
.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var TAG = "VideoPlayer"
private var mediaController: MediaController? = null
private val receiver: BroadcastReceiver? = null
.
.
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
val filter = IntentFilter()
filter.addAction(
"com.ebookfrenzy.videoplayer.VIDEO_INFO")
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context,
intent: Intent) {
Toast.makeText(context,
"Favorite Home Movie Clips",
Toast.LENGTH_LONG).show()
}
}
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
} else {
binding.pipButton.visibility = View.VISIBLE
binding.videoView1.setMediaController(mediaController)
receiver?.let {
unregisterReceiver(it)
}
}
}
Code language: Kotlin (kotlin)
Adding the PiP Action
With the broadcast receiver implemented, the next step is to create a RemoteAction object configured with an image to represent the action within the PiP window. For this example, an image icon file named ic_info_24dp. xml will be used. This file can be found in the project_icons folder of the source code download archive available from the following URL:
https://www.ebookfrenzy.com/web/giraffekotlin/index.php
Locate this icon file and copy and paste it into the app -> res -> drawables folder within the Project tool window:
The next step is to create an Intent that will be sent to the broadcast receiver. This intent then needs to be wrapped up within a PendingIntent object, allowing the intent to be triggered later when the user taps the action button in the PiP window.
Edit the MainActivity.kt file to add a method to create the Intent and PendingIntent objects as follows:
.
.
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
.
.
class MainActivity : AppCompatActivity() {
private val REQUEST_CODE = 101
.
.
private fun createPipAction() {
val actionIntent = Intent("com.ebookfrenzy.videoplayer.VIDEO_INFO")
val pendingIntent = PendingIntent.getBroadcast(this@MainActivity,
REQUEST_CODE, actionIntent, FLAG_IMMUTABLE)
}
}
Code language: Kotlin (kotlin)
Now that both the Intent object and the PendingIntent instance in which it is contained have been created, a RemoteAction object needs to be created containing the icon to appear in the PiP window and the PendingIntent object. Remaining within the createPipAction() method, add this code as follows:
.
.
import android.app.RemoteAction
import android.graphics.drawable.Icon
.
.
private fun createPipAction() {
val actions = ArrayList<RemoteAction>()
val actionIntent = Intent("com.ebookfrenzy.videoplayer.VIDEO_INFO")
val pendingIntent = PendingIntent.getBroadcast(this@MainActivity,
REQUEST_CODE, actionIntent, FLAG_IMMUTABLE)
val icon = Icon.createWithResource(this, R.drawable.ic_info_24dp)
val remoteAction = RemoteAction(icon, "Info", "Video Info", pendingIntent)
actions.add(remoteAction)
}
Code language: Kotlin (kotlin)
Now a PictureInPictureParams object containing the action needs to be created and the parameters applied so that the action appears within the PiP window:
private fun createPipAction() {
val actions = ArrayList<RemoteAction>()
val actionIntent = Intent("com.ebookfrenzy.videoplayer.VIDEO_INFO")
val pendingIntent = PendingIntent.getBroadcast(this@MainActivity,
REQUEST_CODE, actionIntent, FLAG_IMMUTABLE)
val icon =
Icon.createWithResource(this,
R.drawable.ic_info_24dp)
val remoteAction = RemoteAction(icon, "Info",
"Video Info", pendingIntent)
actions.add(remoteAction)
val params = PictureInPictureParams.Builder()
.setActions(actions)
.build()
setPictureInPictureParams(params)
}
Code language: Kotlin (kotlin)
The final task before testing the action is to make a call to the createPipAction() method when the activity enters PiP mode:
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
.
.
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
createPipAction()
} else {
pipButton.visibility = View.VISIBLE
videoView1.setMediaController(mediaController)
.
.
Code language: Kotlin (kotlin)
Testing the Picture-in-Picture Action
Rerun the app and place the activity into PiP mode. Tap on the PiP window so that the new action button appears, as shown in Figure 74-5:
Click on the action button and wait for the Toast message to appear, displaying the name of the video:
Summary
This chapter has demonstrated the addition of Picture-in-Picture support to an Android Studio app project, including enabling and entering PiP mode and implementing a PiP action. This included using a broadcast receiver and pending intents to implement communication between the PiP window and the activity.