diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 0cdaa206c156b..1b8032b7077b8 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -43,6 +43,9 @@ 40% + + false + 250 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index da4bbe81a3e6a..e1475efcdb574 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -104,8 +104,6 @@ public class PipMenuView extends FrameLayout { private static final float MENU_BACKGROUND_ALPHA = 0.3f; private static final float DISABLED_ACTION_ALPHA = 0.54f; - private static final boolean ENABLE_ENTER_SPLIT = true; - private int mMenuState; private boolean mAllowMenuTimeout = true; private boolean mAllowTouches = true; @@ -277,6 +275,8 @@ public class PipMenuView extends FrameLayout { boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { mAllowMenuTimeout = allowMenuTimeout; mDidLastShowMenuResize = resizeMenuOnShow; + final boolean enableEnterSplit = + mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton); if (mMenuState != menuState) { // Disallow touches if the menu needs to resize while showing, and we are transitioning // to/from a full menu state. @@ -297,7 +297,7 @@ public class PipMenuView extends FrameLayout { mDismissButton.getAlpha(), 1f); ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, mEnterSplitButton.getAlpha(), - ENABLE_ENTER_SPLIT && mFocusedTaskAllowSplitScreen ? 1f : 0f); + enableEnterSplit && mFocusedTaskAllowSplitScreen ? 1f : 0f); if (menuState == MENU_STATE_FULL) { mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, enterSplitAnim); diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FeatureFlagManager.java b/packages/SystemUI/src-release/com/android/systemui/flags/FeatureFlagManager.java index 0934b32a71e43..b920a17a0c1ca 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FeatureFlagManager.java +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FeatureFlagManager.java @@ -16,12 +16,15 @@ package com.android.systemui.flags; +import android.content.res.Resources; import android.util.SparseBooleanArray; +import androidx.annotation.BoolRes; import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import java.io.FileDescriptor; @@ -38,8 +41,11 @@ import javax.inject.Inject; @SysUISingleton public class FeatureFlagManager implements FlagReader, FlagWriter, Dumpable { SparseBooleanArray mAccessedFlags = new SparseBooleanArray(); + private Resources mResources; + @Inject - public FeatureFlagManager(DumpManager dumpManager) { + public FeatureFlagManager(DumpManager dumpManager, @Main Resources resources) { + mResources = resources; dumpManager.registerDumpable("SysUIFlags", this); } @@ -51,7 +57,20 @@ public class FeatureFlagManager implements FlagReader, FlagWriter, Dumpable { @Override public boolean isEnabled(BooleanFlag flag) { - return isEnabled(flag.getId(), flag.getDefault()); + boolean def = flag.getDefault(); + if (flag.hasResourceOverride()) { + try { + def = isEnabledInOverlay(flag.getResourceOverride()); + } catch (Resources.NotFoundException e) { + // no-op + } + } + + return isEnabled(flag.getId(), def); + } + + private boolean isEnabledInOverlay(@BoolRes int resId) { + return mResources.getBoolean(resId); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java index 634763866d028..856bbeaa85e46 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java @@ -25,15 +25,14 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; -import android.content.Context; +import android.content.res.Resources; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.util.settings.SecureSettings; import org.junit.After; import org.junit.Before; @@ -53,14 +52,14 @@ import java.io.StringWriter; public class FeatureFlagManagerTest extends SysuiTestCase { FeatureFlagManager mFeatureFlagManager; - @Mock private Context mContext; @Mock private DumpManager mDumpManager; + @Mock private Resources mResources; @Before public void setup() { MockitoAnnotations.initMocks(this); - mFeatureFlagManager = new FeatureFlagManager(mDumpManager); + mFeatureFlagManager = new FeatureFlagManager(mDumpManager, mResources); } @After @@ -70,6 +69,24 @@ public class FeatureFlagManagerTest extends SysuiTestCase { verifyNoMoreInteractions(mDumpManager); } + + @Test + public void testSimpleFlag() { + BooleanFlag flagA = new BooleanFlag(100, false); + BooleanFlag flagB = new BooleanFlag(200, true); + + assertThat(mFeatureFlagManager.isEnabled(flagA)).isFalse(); + assertThat(mFeatureFlagManager.isEnabled(flagB)).isTrue(); + } + + @Test + public void testResourceOverride() { + when(mResources.getBoolean(1)).thenReturn(true); + BooleanFlag flag = new BooleanFlag(100, false, 1); + + assertThat(mFeatureFlagManager.isEnabled(flag)).isTrue(); + } + @Test public void testIsEnabled() { mFeatureFlagManager.setEnabled(1, true); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 393d101e98303..b0e192707fdfb 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2722,9 +2722,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } boolean isResizeable() { + return isResizeable(/* checkPictureInPictureSupport */ true); + } + + boolean isResizeable(boolean checkPictureInPictureSupport) { return mAtmService.mForceResizableActivities || ActivityInfo.isResizeableMode(info.resizeMode) - || info.supportsPictureInPicture() + || (info.supportsPictureInPicture() && checkPictureInPictureSupport) // If the activity can be embedded, it should inherit the bounds of task fragment. || isEmbedded(); } @@ -7679,10 +7683,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // orientation with insets applied. return; } - // Activity should be resizable if the task is. + // Not using Task#isResizeable() or ActivityRecord#isResizeable() directly because app + // compatibility testing showed that android:supportsPictureInPicture="true" alone is not + // sufficient signal for not letterboxing an app. + // TODO(214602463): Remove multi-window check since orientation and aspect ratio + // restrictions should always be applied in multi-window. final boolean isResizeable = task != null - ? task.isResizeable() || isResizeable() - : isResizeable(); + // Activity should be resizable if the task is. + ? task.isResizeable(/* checkPictureInPictureSupport */ false) + || isResizeable(/* checkPictureInPictureSupport */ false) + : isResizeable(/* checkPictureInPictureSupport */ false); if (WindowConfiguration.inMultiWindowMode(windowingMode) && isResizeable) { // Ignore orientation request for resizable apps in multi window. return; @@ -8211,8 +8221,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final float maxAspectRatio = info.getMaxAspectRatio(); final Task rootTask = getRootTask(); final float minAspectRatio = getMinAspectRatio(); + // Not using ActivityRecord#isResizeable() directly because app compatibility testing + // showed that android:supportsPictureInPicture="true" alone is not sufficient signal for + // not letterboxing an app. + // TODO(214602463): Remove multi-window check since orientation and aspect ratio + // restrictions should always be applied in multi-window. if (task == null || rootTask == null - || (inMultiWindowMode() && !shouldCreateCompatDisplayInsets() + || (inMultiWindowMode() && isResizeable(/* checkPictureInPictureSupport */ false) && !fixedOrientationLetterboxed) || (maxAspectRatio < 1 && minAspectRatio < 1 && desiredAspectRatio < 1) || isInVrUiMode(getConfiguration())) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 1e31fdab30d08..42c6dd43ebcee 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4122,9 +4122,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * which controls the visibility and animation of the input method window. */ void updateImeInputAndControlTarget(WindowState target) { - if (target != null && target.mActivityRecord != null) { - target.mActivityRecord.mImeInsetsFrozenUntilStartInput = false; - } if (mImeInputTarget != target) { ProtoLog.i(WM_DEBUG_IME, "setInputMethodInputTarget %s", target); setImeInputTarget(target); @@ -4132,6 +4129,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME)); updateImeControlTarget(); } + // Unfreeze IME insets after the new target updated, in case updateAboveInsetsState may + // deliver unrelated IME insets change to the non-IME requester. + if (target != null && target.mActivityRecord != null) { + target.mActivityRecord.mImeInsetsFrozenUntilStartInput = false; + } } void updateImeControlTarget() { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 50a68450b5f26..fd5df6f6c3906 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2728,10 +2728,14 @@ class Task extends TaskFragment { } boolean isResizeable() { + return isResizeable(/* checkPictureInPictureSupport */ true); + } + + boolean isResizeable(boolean checkPictureInPictureSupport) { final boolean forceResizable = mAtmService.mForceResizableActivities && getActivityType() == ACTIVITY_TYPE_STANDARD; return forceResizable || ActivityInfo.isResizeableMode(mResizeMode) - || mSupportsPictureInPicture; + || (mSupportsPictureInPicture && checkPictureInPictureSupport); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 9a68b5f1b609c..dd4cc5022cd2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -99,6 +99,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; @@ -131,6 +132,7 @@ import android.view.IWindowManager; import android.view.IWindowSession; import android.view.InsetsSource; import android.view.InsetsState; +import android.view.InsetsVisibilities; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.Surface; @@ -147,6 +149,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; @@ -3076,6 +3079,50 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(state.getSource(ITYPE_IME).getFrame(), imeSource.getFrame()); } + @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) + @Test + public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest() + throws RemoteException { + final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1"); + final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2"); + + mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindow( + mImeWindow, null, null); + mImeWindow.getControllableInsetProvider().setServerVisible(true); + + // Simulate app2 is closing and let app1 is visible to be IME targets. + makeWindowVisibleAndDrawn(app1, mImeWindow); + mDisplayContent.setImeLayeringTarget(app1); + mDisplayContent.updateImeInputAndControlTarget(app1); + app2.mActivityRecord.commitVisibility(false, false); + + // app1 requests IME visible. + final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); + requestedVisibilities.setVisibility(ITYPE_IME, true); + app1.setRequestedVisibilities(requestedVisibilities); + mDisplayContent.getInsetsStateController().onInsetsModified(app1); + + // Verify app1's IME insets is visible and app2's IME insets frozen flag set. + assertTrue(app1.getInsetsState().peekSource(ITYPE_IME).isVisible()); + assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); + + // Simulate switching to app2 to make it visible to be IME targets. + makeWindowVisibleAndDrawn(app2); + spyOn(app2); + spyOn(app2.mClient); + ArgumentCaptor insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); + doReturn(true).when(app2).isReadyToDispatchInsetsState(); + mDisplayContent.setImeLayeringTarget(app2); + mDisplayContent.updateImeInputAndControlTarget(app2); + + // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets + // to client if the app didn't request IME visible. + assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); + verify(app2.mClient, atLeastOnce()).insetsChanged(insetsStateCaptor.capture(), anyBoolean(), + anyBoolean()); + assertFalse(insetsStateCaptor.getAllValues().get(0).peekSource(ITYPE_IME).isVisible()); + } + @Test public void testInClosingAnimation_doNotHideSurface() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app");