fix pip feature

This commit is contained in:
Fabian Freund
2026-07-10 06:04:55 +02:00
parent 153f07f28e
commit fa523eb160
4 changed files with 134 additions and 12 deletions
@@ -20,8 +20,10 @@
package eu.weblibre.gecko
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import eu.weblibre.flutter_mozilla_components.HomePressDispatcher
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
@@ -29,9 +31,12 @@ import io.flutter.embedding.engine.FlutterJNI
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterFragmentActivity() {
class MainActivity : FlutterFragmentActivity() {
companion object {
private const val TAG = "MainActivity"
/** Marks the intent [checkAndExitPiP] sends to itself. */
private const val EXTRA_EXIT_PIP = "eu.weblibre.gecko.EXIT_PIP"
}
private val TRIM_MEMORY_CHANNEL = "eu.weblibre.flutter_mozilla_components/trim_memory"
@@ -48,6 +53,45 @@ class MainActivity: FlutterFragmentActivity() {
"cachedEngine=${engineTag(FlutterEngineCache.getInstance().get(ENGINE_ID))}")
super.onCreate(null)
// Restored straight into a pinned task (e.g. the process was killed while in PiP).
checkAndExitPiP()
}
override fun onNewIntent(intent: Intent) {
if (intent.hasExtra(EXTRA_EXIT_PIP)) {
// Our own intent from checkAndExitPiP, delivered back here because
// FLAG_ACTIVITY_REORDER_TO_FRONT re-delivers to a singleTask activity. It carries
// nothing for Flutter to handle, and acting on it again would recurse.
return
}
super.onNewIntent(intent)
checkAndExitPiP()
}
/**
* A picture-in-picture window cannot show a newly opened page, so an intent arriving
* while we are in PiP has to pull the task back to the foreground first.
*/
private fun checkAndExitPiP() {
if (isInPictureInPictureMode) {
moveTaskToBack(false)
startActivity(
Intent(this, MainActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
.putExtra(EXTRA_EXIT_PIP, true)
)
}
}
override fun onUserLeaveHint() {
if (HomePressDispatcher.onUserLeaveHint(this)) {
return
}
super.onUserLeaveHint()
}
/**
@@ -25,19 +25,25 @@ import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.weblibre.flutter_mozilla_components.activities.ExternalAppBrowserActivity
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.ext.toPigeonDownloadState
import eu.weblibre.flutter_mozilla_components.feature.BrowserHandlingScrollFeature
import eu.weblibre.flutter_mozilla_components.feature.GestureAwareSwipeRefreshFeature
import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
import eu.weblibre.flutter_mozilla_components.activities.ExternalAppBrowserActivity
import eu.weblibre.flutter_mozilla_components.services.DownloadService
import io.flutter.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.EngineView
@@ -55,12 +61,12 @@ import mozilla.components.feature.prompts.file.AndroidPhotoPicker
import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature
import eu.weblibre.flutter_mozilla_components.feature.GestureAwareSwipeRefreshFeature
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
@@ -471,6 +477,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view = binding.root,
)
components.core.store.flowScoped(viewLifecycleOwner, Dispatchers.Main) { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(sessionId) }
.distinctUntilChangedBy { tab -> tab.content.pictureInPictureEnabled }
.collect { tab -> pipModeChanged(tab) }
}
mediaSessionFullscreenFeature.set(
feature = MediaSessionFullscreenFeature(
requireActivity(),
@@ -644,8 +656,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
override fun onPictureInPictureModeChanged(enabled: Boolean) {
pictureInPictureFeature?.onPictureInPictureModeChanged(enabled)
if (lifecycle.currentState == androidx.lifecycle.Lifecycle.State.CREATED) {
}
private fun pipModeChanged(session: SessionState) {
if (!session.content.pictureInPictureEnabled && session.content.fullScreen && isAdded) {
onBackPressed()
fullScreenChanged(false)
}
}
@@ -691,6 +707,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
keyboardVisibilityFeature?.checkKeyboardState()
}
@CallSuper
override fun onStop() {
super.onStop()
components.core.store.state.findTabOrCustomTabOrSelectedTab(sessionId)?.let { session ->
if (!session.content.pictureInPictureEnabled && fullScreenFeature.onBackPressed()) {
fullScreenChanged(false)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
@@ -702,6 +729,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserHandlingScrollFeature?.stop()
browserHandlingScrollFeature = null
// Holds the Activity, so it must not outlive the view it was created with.
pictureInPictureFeature = null
GlobalComponents.onPullToRefreshEnabledChanged = null
GlobalComponents.onScreenshotProtectionEnabledChanged = null
val engineView = fragmentEngineView
@@ -0,0 +1,47 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import mozilla.components.support.base.feature.UserInteractionHandler
/**
* Dispatches "the user is leaving the app" to the browser fragments so they can enter
* picture-in-picture.
*
* Lives in this package because Mozilla Components is an `implementation` dependency, so
* [UserInteractionHandler] is not on the app module's compile classpath.
*/
object HomePressDispatcher {
/**
* Returns true if a fragment handled the home press (i.e. entered picture-in-picture),
* in which case the caller must not run the default `onUserLeaveHint` behaviour.
*/
fun onUserLeaveHint(activity: FragmentActivity): Boolean {
// Runtime permission prompts trigger onUserLeaveHint too. That is not the user
// leaving, so it must not put a playing fullscreen video into picture-in-picture.
if (GlobalComponents.components?.notificationsDelegate?.isRequestingPermission == true) {
return false
}
return activity.supportFragmentManager.fragments.any(::dispatch)
}
private fun dispatch(fragment: Fragment): Boolean {
// A hidden or paused fragment must not be able to grab picture-in-picture.
if (!fragment.isResumed) {
return false
}
if (fragment is UserInteractionHandler && fragment.onHomePressed()) {
return true
}
return fragment.childFragmentManager.fragments.any(::dispatch)
}
}
@@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
import eu.weblibre.flutter_mozilla_components.ColorSchemePreference
import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.HomePressDispatcher
import eu.weblibre.flutter_mozilla_components.PwaConstants
import eu.weblibre.flutter_mozilla_components.PwaSessionCreator
import eu.weblibre.flutter_mozilla_components.R
@@ -122,6 +123,14 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
showFragment(sessionId)
}
override fun onUserLeaveHint() {
if (HomePressDispatcher.onUserLeaveHint(this)) {
return
}
super.onUserLeaveHint()
}
private fun waitForComponents(sessionId: String) {
coroutineScope.launch {
var elapsedMs = 0L
@@ -271,12 +280,4 @@ open class ExternalAppBrowserActivity : AppCompatActivity() {
}
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
val fragment = supportFragmentManager.findFragmentById(R.id.container)
if (fragment is ExternalAppBrowserFragment) {
fragment.onPictureInPictureModeChanged(isInPictureInPictureMode)
}
}
}