diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1a62b38..098b551 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,16 +21,21 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/java/org/mosad/teapod/MainActivity.kt b/app/src/main/java/org/mosad/teapod/MainActivity.kt
index 5e74c0a..86e9f2e 100644
--- a/app/src/main/java/org/mosad/teapod/MainActivity.kt
+++ b/app/src/main/java/org/mosad/teapod/MainActivity.kt
@@ -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)
diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
index d5b76e9..0b82c0d 100644
--- a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
+++ b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt
@@ -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) {
@@ -229,7 +308,7 @@ class PlayerActivity : AppCompatActivity() {
}
}
- private fun releasePlayer(){
+ private fun releasePlayer() {
playbackPosition = model.player.currentPosition
currentWindow = model.player.currentWindowIndex
playWhenReady = model.player.playWhenReady
@@ -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 (controller.isVisible) controller.hide() else controller.show()
+ if (!isInPiPMode()) {
+ if (controller.isVisible) controller.hide() else controller.show()
+ }
+
return true
}
diff --git a/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt b/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
new file mode 100644
index 0000000..6d3af80
--- /dev/null
+++ b/app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
@@ -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
+ }
+ }
+}
+