Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
agp = "8.9.1"
fragmentCompose = "1.8.6"
kotlin = "2.1.10"
coreKtx = "1.17.0-alpha01"
coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
Expand Down Expand Up @@ -103,9 +103,9 @@ play-services-location = { module = "com.google.android.gms:play-services-locati
# Core dependencies
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }

androidx-activity = "androidx.activity:activity-ktx:1.10.0"
androidx-activity = "androidx.activity:activity-ktx:1.13.0-alpha01"

androidx-core = "androidx.core:core-ktx:1.12.0"
androidx-core = "androidx.core:core-ktx:1.18.0-alpha01"
androidx-exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
# Fragment 1.7.0 alpha and Transition 1.5.0 alpha are required for predictive back to work with Fragments and transitions
androidx-fragment = "androidx.fragment:fragment-ktx:1.7.0-alpha10"
Expand Down Expand Up @@ -170,6 +170,7 @@ androidx-window-rxjava2 = { module = "androidx.window:window-rxjava2", version.r
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" }
androidx-media = "androidx.media:media:1.7.0"
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
androidx-corepip = "androidx.core:core-pip:1.0.0-SNAPSHOT"
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
androidx-draganddrop = "androidx.draganddrop:draganddrop:1.0.0"
androidx-dynamicanimation = "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ android {
compileSdk = 36

defaultConfig {
minSdk = 21
minSdk = 23
targetSdk = 35

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand All @@ -45,6 +45,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.media)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.corepip)

// Testing
androidTestImplementation(libs.androidx.test.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


package com.example.android.pip

import android.app.PictureInPictureParams
import android.app.PictureInPictureUiState

import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.support.v4.media.MediaMetadataCompat
Expand All @@ -32,45 +31,61 @@ import android.util.Rational
import android.view.View
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.core.app.PictureInPictureParamsCompat
import androidx.core.pip.PictureInPictureDelegate
import androidx.core.pip.VideoPlaybackPictureInPicture
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.doOnLayout
import com.example.android.pip.databinding.PipMovieActivityBinding
import com.example.android.pip.widget.MovieView


/**
* Demonstrates usage of Picture-in-Picture when using [MediaSessionCompat].
*/
@RequiresApi(Build.VERSION_CODES.O)
class PiPMovieActivity : ComponentActivity() {
class PiPMovieActivity : ComponentActivity(),
PictureInPictureDelegate.OnPictureInPictureEventListener {


companion object {


private const val TAG = "MediaSessionPlaybackActivity"


private const val MEDIA_ACTIONS_PLAY_PAUSE =
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE


private const val MEDIA_ACTIONS_ALL =
MEDIA_ACTIONS_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS


private const val PLAYLIST_SIZE = 2
}


private lateinit var binding: PipMovieActivityBinding


private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture


private lateinit var session: MediaSessionCompat


/**
* Callbacks from the [MovieView] showing the video playback.
*/
private val movieListener = object : MovieView.MovieListener() {


override fun onMovieStarted() {
// We are playing the video now. Update the media session state and the PiP window will
// update the actions.
Expand All @@ -81,6 +96,7 @@ class PiPMovieActivity : ComponentActivity() {
)
}


override fun onMovieStopped() {
// The video stopped or reached its end. Update the media session state and the PiP
// window will update the actions.
Expand All @@ -90,50 +106,63 @@ class PiPMovieActivity : ComponentActivity() {
binding.movie.getVideoResourceId(),
)
}

override fun onMovieMinimized() {
// The MovieView wants us to minimize it. We enter Picture-in-Picture mode now.
minimize()
}
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PipMovieActivityBinding.inflate(layoutInflater)
pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
pictureInPictureImpl.setPlayerView(binding.movie)
pictureInPictureImpl.setEnabled(true)


setContentView(binding.root)


try {
Linkify.addLinks(binding.explanation, Linkify.WEB_URLS)
} catch (e: Exception) {
Log.w("PiP", "Failed to add links", e)
}
binding.pip.setOnClickListener { minimize() }

// Configure parameters for the picture-in-picture mode. We do this at the first layout of
// the MovieView because we use its layout position and size.
binding.movie.doOnLayout { updatePictureInPictureParams() }

binding.pip.setOnClickListener {
enterPictureInPictureMode(updatePictureInPictureParams())
}
// Set up the video; it automatically starts.
binding.movie.setMovieListener(movieListener)
}


override fun onStart() {
super.onStart()
initializeMediaSession()
}


private fun updatePictureInPictureParams(): PictureInPictureParamsCompat {
return PictureInPictureParamsCompat.Builder()
.setAspectRatio(Rational(binding.movie.width, binding.movie.height))
.build()
}


private fun initializeMediaSession() {
session = MediaSessionCompat(this, TAG)
session.isActive = true
MediaControllerCompat.setMediaController(this, session.controller)


val metadata = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, binding.movie.title)
.build()
session.setMetadata(metadata)


session.setCallback(MediaSessionCallback(binding.movie))


val state = if (binding.movie.isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
Expand All @@ -147,6 +176,7 @@ class PiPMovieActivity : ComponentActivity() {
)
}


override fun onStop() {
super.onStop()
// On entering Picture-in-Picture mode, onPause is called, but not onStop.
Expand All @@ -155,6 +185,7 @@ class PiPMovieActivity : ComponentActivity() {
session.release()
}


override fun onRestart() {
super.onRestart()
if (!isInPictureInPictureMode) {
Expand All @@ -163,74 +194,27 @@ class PiPMovieActivity : ComponentActivity() {
}
}


override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
adjustFullScreen(newConfig)
}


override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
adjustFullScreen(resources.configuration)
}
}

override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean, newConfig: Configuration,
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
// Hide the controls in picture-in-picture mode.
binding.movie.hideControls()
} else {
// Show the video controls if the video is not playing
if (!binding.movie.isPlaying) {
binding.movie.showControls()
}
}
}

@RequiresApi(35)
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
super.onPictureInPictureUiStateChanged(pipState)
if (pipState.isTransitioningToPip) {
binding.movie.hideControls()
}
}


private fun updatePictureInPictureParams(): PictureInPictureParams {
// Calculate the aspect ratio of the PiP screen.
val aspectRatio = Rational(binding.movie.width, binding.movie.height)
// The movie view turns into the picture-in-picture mode.
val visibleRect = Rect()
binding.movie.getGlobalVisibleRect(visibleRect)
val params = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
// Specify the portion of the screen that turns into the picture-in-picture mode.
// This makes the transition animation smoother.
.setSourceRectHint(visibleRect)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// The screen automatically turns into the picture-in-picture mode when it is hidden
// by the "Home" button.
params.setAutoEnterEnabled(true)
}
return params.build().also {
setPictureInPictureParams(it)
}
}

/**
* Enters Picture-in-Picture mode.
*/
private fun minimize() {
enterPictureInPictureMode(updatePictureInPictureParams())
}

/**
* Adjusts immersive full-screen flags depending on the screen orientation.


* @param config The current [Configuration].
*/
private fun adjustFullScreen(config: Configuration) {
Expand All @@ -248,9 +232,11 @@ class PiPMovieActivity : ComponentActivity() {
}
}


/**
* Overloaded method that persists previously set media actions.


* @param state The state of the video, e.g. playing, paused, etc.
* @param position The position of playback in the video.
* @param mediaId The media id related to the video in the media session.
Expand All @@ -264,6 +250,7 @@ class PiPMovieActivity : ComponentActivity() {
updatePlaybackState(state, actions, position, mediaId)
}


private fun updatePlaybackState(
@PlaybackStateCompat.State state: Int,
playbackActions: Long,
Expand All @@ -277,6 +264,15 @@ class PiPMovieActivity : ComponentActivity() {
session.setPlaybackState(builder.build())
}


override fun onPictureInPictureEvent(
event: PictureInPictureDelegate.Event,
config: Configuration?,
) {
TODO("Not yet implemented")
}
Comment on lines +268 to +273
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The onPictureInPictureEvent callback is left as a TODO. This is a critical omission as it's responsible for handling PiP lifecycle events, which replaces the old onPictureInPictureModeChanged logic. Without implementing this, UI elements like player controls won't be correctly shown or hidden when entering or exiting PiP mode.

    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?,
    ) {
        when (event) {
            PictureInPictureDelegate.Event.EVENT_ENTER -> {
                // Hide the controls in picture-in-picture mode.
                binding.movie.hideControls()
            }
            PictureInPictureDelegate.Event.EVENT_EXIT -> {
                // Show the video controls if the video is not playing
                if (!binding.movie.isPlaying) {
                    binding.movie.showControls()
                }
            }
            else -> {
                // Other events are not handled in this sample.
            }
        }
    }



/**
* Updates the [MovieView] based on the callback actions. <br></br>
* Simulates a playlist that will disable actions when you cannot skip through the playlist in a
Expand All @@ -286,16 +282,20 @@ class PiPMovieActivity : ComponentActivity() {
private val movieView: MovieView,
) : MediaSessionCompat.Callback() {


private var indexInPlaylist: Int = 1


override fun onPlay() {
movieView.play()
}


override fun onPause() {
movieView.pause()
}


override fun onSkipToNext() {
movieView.startVideo()
if (indexInPlaylist < PLAYLIST_SIZE) {
Expand All @@ -318,6 +318,7 @@ class PiPMovieActivity : ComponentActivity() {
}
}


override fun onSkipToPrevious() {
movieView.startVideo()
if (indexInPlaylist > 0) {
Expand Down
Loading
Loading