From e5037cf9ac83836a77bd212a2304f289d75a26b4 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 6 Jan 2021 15:07:31 +0100 Subject: [PATCH 1/3] add pip support to player activity --- app/src/main/AndroidManifest.xml | 13 ++-- .../org/mosad/teapod/player/PlayerActivity.kt | 69 +++++++++++++++++-- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a62b38..ab96abf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,16 +21,19 @@ - + + \ No newline at end of file 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..5c328d2 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,16 @@ package org.mosad.teapod.player import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint +import android.app.PictureInPictureParams +import android.content.pm.PackageManager +import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.util.Log +import android.util.Rational import android.view.* import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible @@ -73,6 +78,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 +93,8 @@ class PlayerActivity : AppCompatActivity() { override fun onResume() { super.onResume() + if (isInPiPMode()) { return } + if (Util.SDK_INT <= 23) { initPlayer() video_view?.onResume() @@ -91,6 +103,8 @@ class PlayerActivity : AppCompatActivity() { override fun onPause() { super.onPause() + if (isInPiPMode()) { return } + if (Util.SDK_INT <= 23) { video_view?.onPause() releasePlayer() @@ -112,6 +126,38 @@ class PlayerActivity : AppCompatActivity() { super.onSaveInstanceState(outState) } + /** + * 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) + } + } + } + + 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 +224,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 +261,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 +277,7 @@ class PlayerActivity : AppCompatActivity() { } } - private fun releasePlayer(){ + private fun releasePlayer() { playbackPosition = model.player.currentPosition currentWindow = model.player.currentWindowIndex playWhenReady = model.player.playWhenReady @@ -378,6 +426,14 @@ class PlayerActivity : AppCompatActivity() { pauseAndHideControls() } + private fun isInPiPMode(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isInPictureInPictureMode + } else { + false // pip mode not supported + } + } + /** * pause playback and hide controls */ @@ -393,7 +449,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 } -- 2.44.0 From 86e07ba2cf9c3bf900659da676cad0984afe0c06 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 8 Jan 2021 09:31:50 +0100 Subject: [PATCH 2/3] return to main activity if pip was launched before --- app/src/main/AndroidManifest.xml | 3 ++ .../org/mosad/teapod/player/PlayerActivity.kt | 31 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab96abf..26084de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,9 @@ android:label="@string/app_name" android:theme="@style/PlayerTheme" android:supportsPictureInPicture="true" + android:launchMode="singleTask" + android:excludeFromRecents="true" + android:taskAffinity=".player.PlayerActivity" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"> 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 5c328d2..5f244b2 100644 --- a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt @@ -3,7 +3,10 @@ package org.mosad.teapod.player import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint +import android.app.ActivityManager import android.app.PictureInPictureParams +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.os.Build @@ -35,6 +38,7 @@ import java.util.* import java.util.concurrent.TimeUnit import kotlin.concurrent.scheduleAtFixedRate + class PlayerActivity : AppCompatActivity() { private val model: PlayerViewModel by viewModels() @@ -43,6 +47,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 @@ -117,6 +122,10 @@ class PlayerActivity : AppCompatActivity() { video_view?.onPause() releasePlayer() } + + if (wasInPIP) { + navToLauncherTask() + } } override fun onSaveInstanceState(outState: Bundle) { @@ -146,10 +155,15 @@ class PlayerActivity : AppCompatActivity() { .build() enterPictureInPictureMode(params) } + + wasInPIP = isInPiPMode() } } - override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration? + ) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode. @@ -434,6 +448,21 @@ class PlayerActivity : AppCompatActivity() { } } + /** + * Bring up launcher task to front + */ + private fun navToLauncherTask() { + val activityManager = (this.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 + } + } + } + /** * pause playback and hide controls */ -- 2.44.0 From 8e8db386a0f5de6448d22711c3b331d4db0abb84 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 8 Jan 2021 10:58:24 +0100 Subject: [PATCH 3/3] play new media if selected while player is in pip & minor code clean up move some player avtivity stuff to ActivityUtils --- app/src/main/AndroidManifest.xml | 13 ++- .../java/org/mosad/teapod/MainActivity.kt | 3 + .../org/mosad/teapod/player/PlayerActivity.kt | 87 ++++++++----------- .../org/mosad/teapod/util/ActivityUtils.kt | 57 ++++++++++++ 4 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 26084de..098b551 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,15 +28,14 @@ - + android:theme="@style/PlayerTheme" /> \ 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 5f244b2..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,9 +3,7 @@ package org.mosad.teapod.player import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint -import android.app.ActivityManager import android.app.PictureInPictureParams -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration @@ -13,7 +11,9 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.util.Rational -import android.view.* +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 @@ -34,11 +34,13 @@ 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 - class PlayerActivity : AppCompatActivity() { private val model: PlayerViewModel by viewModels() @@ -47,7 +49,7 @@ class PlayerActivity : AppCompatActivity() { private lateinit var gestureDetector: GestureDetectorCompat private lateinit var timerUpdates: TimerTask - private var wasInPIP = false + private var wasInPiP = false private var playWhenReady = true private var currentWindow = 0 private var playbackPosition: Long = 0 @@ -123,9 +125,8 @@ class PlayerActivity : AppCompatActivity() { releasePlayer() } - if (wasInPIP) { - navToLauncherTask() - } + // if the player was in pip, it's on a different task + if (wasInPiP) { navToLauncherTask() } } override fun onSaveInstanceState(outState: Bundle) { @@ -135,6 +136,22 @@ 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 */ @@ -156,7 +173,7 @@ class PlayerActivity : AppCompatActivity() { enterPictureInPictureMode(params) } - wasInPIP = isInPiPMode() + wasInPiP = isInPiPMode() } } @@ -322,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 + } } /** @@ -374,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 @@ -440,29 +444,6 @@ class PlayerActivity : AppCompatActivity() { pauseAndHideControls() } - private fun 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 - */ - private fun navToLauncherTask() { - val activityManager = (this.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 - } - } - } - /** * pause playback and hide controls */ 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 + } + } +} + -- 2.44.0