Merge pull request 'Player PiP' (#24) from feature/pip_activity into develop

Reviewed-on: #24
This commit is contained in:
Jannik 2021-01-08 11:01:31 +01:00
commit 8c0f4965e7
4 changed files with 166 additions and 32 deletions

View File

@ -21,16 +21,21 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".player.PlayerActivity"
android:label="@string/app_name"
android:theme="@style/PlayerTheme"
android:configChanges="orientation|screenSize|layoutDirection" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
</activity> </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> </application>
</manifest> </manifest>

View File

@ -165,6 +165,9 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
} }
} }
/**
* start the player as new activity
*/
fun startPlayer(mediaId: Int, episodeId: Int) { fun startPlayer(mediaId: Int, episodeId: Int) {
val intent = Intent(this, PlayerActivity::class.java).apply { val intent = Intent(this, PlayerActivity::class.java).apply {
putExtra(getString(R.string.intent_media_id), mediaId) putExtra(getString(R.string.intent_media_id), mediaId)

View File

@ -3,11 +3,19 @@ package org.mosad.teapod.player
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint 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.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log 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.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible 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.EpisodesListPlayer
import org.mosad.teapod.ui.components.LanguageSettingsPlayer import org.mosad.teapod.ui.components.LanguageSettingsPlayer
import org.mosad.teapod.util.DataTypes 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.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.scheduleAtFixedRate
@ -38,6 +49,7 @@ class PlayerActivity : AppCompatActivity() {
private lateinit var gestureDetector: GestureDetectorCompat private lateinit var gestureDetector: GestureDetectorCompat
private lateinit var timerUpdates: TimerTask private lateinit var timerUpdates: TimerTask
private var wasInPiP = false
private var playWhenReady = true private var playWhenReady = true
private var currentWindow = 0 private var currentWindow = 0
private var playbackPosition: Long = 0 private var playbackPosition: Long = 0
@ -73,6 +85,11 @@ class PlayerActivity : AppCompatActivity() {
initActions() 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() { override fun onStart() {
super.onStart() super.onStart()
if (Util.SDK_INT > 23) { if (Util.SDK_INT > 23) {
@ -83,6 +100,8 @@ class PlayerActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (isInPiPMode()) { return }
if (Util.SDK_INT <= 23) { if (Util.SDK_INT <= 23) {
initPlayer() initPlayer()
video_view?.onResume() video_view?.onResume()
@ -91,6 +110,8 @@ class PlayerActivity : AppCompatActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (isInPiPMode()) { return }
if (Util.SDK_INT <= 23) { if (Util.SDK_INT <= 23) {
video_view?.onPause() video_view?.onPause()
releasePlayer() releasePlayer()
@ -103,6 +124,9 @@ class PlayerActivity : AppCompatActivity() {
video_view?.onPause() video_view?.onPause()
releasePlayer() releasePlayer()
} }
// if the player was in pip, it's on a different task
if (wasInPiP) { navToLauncherTask() }
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -112,6 +136,59 @@ class PlayerActivity : AppCompatActivity() {
super.onSaveInstanceState(outState) 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() { private fun initPlayer() {
if (model.media.id < 0) { if (model.media.id < 0) {
Log.e(javaClass.name, "No media was set.") Log.e(javaClass.name, "No media was set.")
@ -178,7 +255,9 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initActions() { private fun initActions() {
exo_close_player.setOnClickListener { this.finish() } exo_close_player.setOnClickListener {
this.finish()
}
rwd_10.setOnButtonClickListener { rewind() } rwd_10.setOnButtonClickListener { rewind() }
ffwd_10.setOnButtonClickListener { fastForward() } ffwd_10.setOnButtonClickListener { fastForward() }
button_next_ep.setOnClickListener { playNextEpisode() } button_next_ep.setOnClickListener { playNextEpisode() }
@ -213,8 +292,8 @@ class PlayerActivity : AppCompatActivity() {
} }
if (remainingTime in 1..20000) { if (remainingTime in 1..20000) {
// if the next ep button is not visible, make it visible // if the next ep button is not visible, make it visible. Don't show in pip mode
if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay) { if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) {
withContext(Dispatchers.Main) { showButtonNextEp() } withContext(Dispatchers.Main) { showButtonNextEp() }
} }
} else if (btnNextEpIsVisible) { } else if (btnNextEpIsVisible) {
@ -229,7 +308,7 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
private fun releasePlayer(){ private fun releasePlayer() {
playbackPosition = model.player.currentPosition playbackPosition = model.player.currentPosition
currentWindow = model.player.currentWindowIndex currentWindow = model.player.currentWindowIndex
playWhenReady = model.player.playWhenReady playWhenReady = model.player.playWhenReady
@ -260,11 +339,19 @@ class PlayerActivity : AppCompatActivity() {
private fun onMediaChanged() { private fun onMediaChanged() {
exo_text_title.text = model.getMediaTitle() exo_text_title.text = model.getMediaTitle()
// hide the next ep button, if there is none
button_next_ep_c.visibility = if (model.nextEpisode == null) { button_next_ep_c.visibility = if (model.nextEpisode == null) {
View.GONE View.GONE
} else { } else {
View.VISIBLE 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() 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 * show the next episode button
* TODO improve the show animation * TODO improve the show animation
@ -393,7 +459,10 @@ class PlayerActivity : AppCompatActivity() {
* on single tap hide or show the controls * on single tap hide or show the controls
*/ */
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
if (controller.isVisible) controller.hide() else controller.show() if (!isInPiPMode()) {
if (controller.isVisible) controller.hide() else controller.show()
}
return true return true
} }

View File

@ -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
}
}
}