Flutter PIP Plugin - Android Support Auto Enter PiP Mode below Android 12
Project repository PIP, also published on pub.dev pip 0.0.3. Your stars and likes will be my greatest motivation for further improvements. According to the development documentation Add videos using picture-in-picture (PiP), the PIP feature was introduced in Android 8.0 (API level 26), but the autoEnter functionality was only supported starting from Android 12. For versions that don't support it, we need to actively call enterPictureInPictureMode by listening to onUserLeaveHint to enter PIP Mode. In previous versions, because FlutterActivity didn't forward onUserLeaveHint, we could only use flutter's didChangeAppLifecycleState event in dart to actively call the method when the app went to the background to enter PIP Mode. However, in practice, it seems impossible to distinguish notifications from the notification bar, which caused the app to automatically enter PIP Mode even when it was in the foreground when users pulled down the notification bar. This was obviously not what we wanted. The optimized effect is as follows: Modifying the PIP Plugin Adding PipActivity package org.opentraa.pip; import android.app.PictureInPictureUiState; import android.content.res.Configuration; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import io.flutter.embedding.android.FlutterActivity; @RequiresApi(Build.VERSION_CODES.O) public class PipActivity extends FlutterActivity { public interface PipActivityListener { void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig); void onPictureInPictureUiStateChanged(PictureInPictureUiState state); boolean onPictureInPictureRequested(); void onUserLeaveHint(); } private PipActivityListener mListener; public void setPipActivityListener(PipActivityListener listener) { mListener = listener; } // only available in API level 26 and above @RequiresApi(26) @Override public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); if (mListener != null) { mListener.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); } } // only available in API level 30 and above @RequiresApi(30) @Override public boolean onPictureInPictureRequested() { if (mListener != null) { return mListener.onPictureInPictureRequested(); } return super.onPictureInPictureRequested(); } // only available in API level 31 and above @RequiresApi(31) @Override public void onPictureInPictureUiStateChanged(@NonNull PictureInPictureUiState state) { super.onPictureInPictureUiStateChanged(state); if (mListener != null) { mListener.onPictureInPictureUiStateChanged(state); } } @Override public void onUserLeaveHint() { super.onUserLeaveHint(); if (mListener != null) { mListener.onUserLeaveHint(); } } } The main idea is that if users of the PIP plugin want to support automatic entry into PIP Mode when the app goes to the background on Android 12 and below, they can modify their MainActivity's parent class to PipActivity. This way, when the PIP plugin is registered, it can determine whether to enable related functionality by checking if the passed Activity is a PipActivity. Binding Activity to PipController PipPlugin initializes PipController in onAttachedToActivity and onReattachedToActivityForConfigChanges private void initPipController(@NonNull ActivityPluginBinding binding) { if (pipController == null) { pipController = new PipController( binding.getActivity(), new PipController.PipStateChangedListener() { @Override public void onPipStateChangedListener( PipController.PipState state) { // put state into a json object channel.invokeMethod("stateChanged", new HashMap() { { put("state", state.getValue()); } }); } }); } else { pipController.attachToActivity(binding.getActivity()); } } @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { initPipController(binding); } @Override public void onReattachedToActivityForConfigChanges( @NonNull ActivityPluginBinding binding) { initPipController(binding); } In the PipController constructor and attachToActivity method, check if autoEnter is supported based on the current system version and bound Activity public PipController(@NonNull Activity activity,

Project repository PIP, also published on pub.dev pip 0.0.3. Your stars and likes will be my greatest motivation for further improvements.
According to the development documentation Add videos using picture-in-picture (PiP), the PIP feature was introduced in Android 8.0 (API level 26), but the autoEnter functionality was only supported starting from Android 12. For versions that don't support it, we need to actively call enterPictureInPictureMode by listening to onUserLeaveHint to enter PIP Mode. In previous versions, because FlutterActivity didn't forward onUserLeaveHint, we could only use flutter's didChangeAppLifecycleState event in dart to actively call the method when the app went to the background to enter PIP Mode. However, in practice, it seems impossible to distinguish notifications from the notification bar, which caused the app to automatically enter PIP Mode even when it was in the foreground when users pulled down the notification bar. This was obviously not what we wanted. The optimized effect is as follows:
Modifying the PIP Plugin
- Adding PipActivity
package org.opentraa.pip;
import android.app.PictureInPictureUiState;
import android.content.res.Configuration;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import io.flutter.embedding.android.FlutterActivity;
@RequiresApi(Build.VERSION_CODES.O)
public class PipActivity extends FlutterActivity {
public interface PipActivityListener {
void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig);
void onPictureInPictureUiStateChanged(PictureInPictureUiState state);
boolean onPictureInPictureRequested();
void onUserLeaveHint();
}
private PipActivityListener mListener;
public void setPipActivityListener(PipActivityListener listener) {
mListener = listener;
}
// only available in API level 26 and above
@RequiresApi(26)
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (mListener != null) {
mListener.onPictureInPictureModeChanged(isInPictureInPictureMode,
newConfig);
}
}
// only available in API level 30 and above
@RequiresApi(30)
@Override
public boolean onPictureInPictureRequested() {
if (mListener != null) {
return mListener.onPictureInPictureRequested();
}
return super.onPictureInPictureRequested();
}
// only available in API level 31 and above
@RequiresApi(31)
@Override
public void
onPictureInPictureUiStateChanged(@NonNull PictureInPictureUiState state) {
super.onPictureInPictureUiStateChanged(state);
if (mListener != null) {
mListener.onPictureInPictureUiStateChanged(state);
}
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
if (mListener != null) {
mListener.onUserLeaveHint();
}
}
}
The main idea is that if users of the PIP plugin want to support automatic entry into PIP Mode when the app goes to the background on Android 12 and below, they can modify their MainActivity's parent class to PipActivity. This way, when the PIP plugin is registered, it can determine whether to enable related functionality by checking if the passed Activity is a PipActivity.
- Binding Activity to PipController PipPlugin initializes PipController in onAttachedToActivity and onReattachedToActivityForConfigChanges
private void initPipController(@NonNull ActivityPluginBinding binding) {
if (pipController == null) {
pipController = new PipController(
binding.getActivity(), new PipController.PipStateChangedListener() {
@Override
public void onPipStateChangedListener(
PipController.PipState state) {
// put state into a json object
channel.invokeMethod("stateChanged",
new HashMap<String, Object>() {
{ put("state", state.getValue()); }
});
}
});
} else {
pipController.attachToActivity(binding.getActivity());
}
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
initPipController(binding);
}
@Override
public void onReattachedToActivityForConfigChanges(
@NonNull ActivityPluginBinding binding) {
initPipController(binding);
}
- In the PipController constructor and attachToActivity method, check if autoEnter is supported based on the current system version and bound Activity
public PipController(@NonNull Activity activity,
@Nullable PipStateChangedListener listener) {
setActivity(activity);
// ... Other code ...
}
private boolean checkAutoEnterSupport() {
// Android 12 and above support to set auto enter enabled directly
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return true;
}
// For android 11 and below, we need to check if the activity is kind of
// PipActivity since we can enter pip mode when the onUserLeaveHint is
// called to enter pip mode as a workaround
Activity activity = mActivity.get();
return activity instanceof PipActivity;
}
private void setActivity(Activity activity) {
mActivity = new WeakReference<>(activity);
if (activity instanceof PipActivity) {
((PipActivity)activity).setPipActivityListener(this);
}
mIsSupported = checkPipSupport();
mIsAutoEnterSupported = checkAutoEnterSupport();
}
public void attachToActivity(@NonNull Activity activity) {
setActivity(activity);
}
Modifying MainActivity in the Example Project
- Simple MainActivity
package org.opentraa.pip_example;
import io.flutter.embedding.android.FlutterActivity;
import org.opentraa.pip.PipActivity;
public class MainActivity extends PipActivity {
}
As shown above, we have now supported PIP Mode autoEnter functionality for all versions.