Merge pull request 'Player PiP' (#24) from feature/pip_activity into develop
Reviewed-on: #24
This commit is contained in:
		| @ -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) { | ||||
| @ -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 | ||||
|         } | ||||
|  | ||||
|  | ||||
							
								
								
									
										57
									
								
								app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/src/main/java/org/mosad/teapod/util/ActivityUtils.kt
									
									
									
									
									
										Normal 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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user