Player PiP #24
|
@ -21,16 +21,21 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".player.PlayerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/PlayerTheme"
|
||||
android:configChanges="orientation|screenSize|layoutDirection" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".player.PlayerActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=".player.PlayerActivity"
|
||||
android:theme="@style/PlayerTheme" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -165,6 +165,9 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* start the player as new activity
|
||||
*/
|
||||
fun startPlayer(mediaId: Int, episodeId: Int) {
|
||||
val intent = Intent(this, PlayerActivity::class.java).apply {
|
||||
putExtra(getString(R.string.intent_media_id), mediaId)
|
||||
|
|
|
@ -3,11 +3,19 @@ package org.mosad.teapod.player
|
|||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.util.Rational
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -26,6 +34,9 @@ import org.mosad.teapod.preferences.Preferences
|
|||
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
||||
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
|
||||
import org.mosad.teapod.util.DataTypes
|
||||
import org.mosad.teapod.util.hideBars
|
||||
import org.mosad.teapod.util.isInPiPMode
|
||||
import org.mosad.teapod.util.navToLauncherTask
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
|
@ -38,6 +49,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||
private lateinit var gestureDetector: GestureDetectorCompat
|
||||
private lateinit var timerUpdates: TimerTask
|
||||
|
||||
private var wasInPiP = false
|
||||
private var playWhenReady = true
|
||||
private var currentWindow = 0
|
||||
private var playbackPosition: Long = 0
|
||||
|
@ -73,6 +85,11 @@ class PlayerActivity : AppCompatActivity() {
|
|||
initActions()
|
||||
}
|
||||
|
||||
/**
|
||||
* once minimum is android 7.0 this can be simplified
|
||||
* only onStart and onStop should be needed then
|
||||
* see: https://developer.android.com/guide/topics/ui/picture-in-picture#continuing_playback
|
||||
*/
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (Util.SDK_INT > 23) {
|
||||
|
@ -83,6 +100,8 @@ class PlayerActivity : AppCompatActivity() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isInPiPMode()) { return }
|
||||
|
||||
if (Util.SDK_INT <= 23) {
|
||||
initPlayer()
|
||||
video_view?.onResume()
|
||||
|
@ -91,6 +110,8 @@ class PlayerActivity : AppCompatActivity() {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (isInPiPMode()) { return }
|
||||
|
||||
if (Util.SDK_INT <= 23) {
|
||||
video_view?.onPause()
|
||||
releasePlayer()
|
||||
|
@ -103,6 +124,9 @@ class PlayerActivity : AppCompatActivity() {
|
|||
video_view?.onPause()
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
// if the player was in pip, it's on a different task
|
||||
if (wasInPiP) { navToLauncherTask() }
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
@ -112,6 +136,59 @@ class PlayerActivity : AppCompatActivity() {
|
|||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
/**
|
||||
* used, when the player is in pip and the user selects a new media
|
||||
*/
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
// when the intent changed, lead the new media and play it
|
||||
intent?.let {
|
||||
model.loadMedia(
|
||||
it.getIntExtra(getString(R.string.intent_media_id), 0),
|
||||
it.getIntExtra(getString(R.string.intent_episode_id), 0)
|
||||
)
|
||||
model.playEpisode(model.currentEpisode, replace = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* previous to android n, don't override
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
|
||||
// start pip mode, if supported
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("deprecation")
|
||||
enterPictureInPictureMode()
|
||||
} else {
|
||||
val width = model.player.videoFormat?.width ?: 0
|
||||
val height = model.player.videoFormat?.height ?: 0
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(width, height))
|
||||
.build()
|
||||
enterPictureInPictureMode(params)
|
||||
}
|
||||
|
||||
wasInPiP = isInPiPMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
|
||||
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
|
||||
if (isInPictureInPictureMode) {
|
||||
controller.hideImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPlayer() {
|
||||
if (model.media.id < 0) {
|
||||
Log.e(javaClass.name, "No media was set.")
|
||||
|
@ -178,7 +255,9 @@ class PlayerActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun initActions() {
|
||||
exo_close_player.setOnClickListener { this.finish() }
|
||||
exo_close_player.setOnClickListener {
|
||||
this.finish()
|
||||
}
|
||||
rwd_10.setOnButtonClickListener { rewind() }
|
||||
ffwd_10.setOnButtonClickListener { fastForward() }
|
||||
button_next_ep.setOnClickListener { playNextEpisode() }
|
||||
|
@ -213,8 +292,8 @@ class PlayerActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
if (remainingTime in 1..20000) {
|
||||
// if the next ep button is not visible, make it visible
|
||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay) {
|
||||
// if the next ep button is not visible, make it visible. Don't show in pip mode
|
||||
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
|
||||
withContext(Dispatchers.Main) { showButtonNextEp() }
|
||||
}
|
||||
} else if (btnNextEpIsVisible) {
|
||||
|
@ -260,11 +339,19 @@ class PlayerActivity : AppCompatActivity() {
|
|||
private fun onMediaChanged() {
|
||||
exo_text_title.text = model.getMediaTitle()
|
||||
|
||||
// hide the next ep button, if there is none
|
||||
button_next_ep_c.visibility = if (model.nextEpisode == null) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
// hide the episodes button, if the media type changed
|
||||
button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -312,27 +399,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||
hideButtonNextEp()
|
||||
}
|
||||
|
||||
/**
|
||||
* hide the status and navigation bar
|
||||
*/
|
||||
private fun hideBars() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
window.insetsController?.apply {
|
||||
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@Suppress("deprecation")
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* show the next episode button
|
||||
* TODO improve the show animation
|
||||
|
@ -393,7 +459,10 @@ class PlayerActivity : AppCompatActivity() {
|
|||
* on single tap hide or show the controls
|
||||
*/
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
if (!isInPiPMode()) {
|
||||
if (controller.isVisible) controller.hide() else controller.show()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package org.mosad.teapod.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
|
||||
/**
|
||||
* hide the status and navigation bar
|
||||
*/
|
||||
fun Activity.hideBars() {
|
||||
window.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setDecorFitsSystemWindows(false)
|
||||
insetsController?.apply {
|
||||
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@Suppress("deprecation")
|
||||
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.isInPiPMode(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInPictureInPictureMode
|
||||
} else {
|
||||
false // pip mode not supported
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring up launcher task to front
|
||||
*/
|
||||
fun Activity.navToLauncherTask() {
|
||||
val activityManager = (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
|
||||
activityManager.appTasks.forEach { task ->
|
||||
val baseIntent = task.taskInfo.baseIntent
|
||||
val categories = baseIntent.categories
|
||||
if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
|
||||
task.moveToFront()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue