diff options
Diffstat (limited to 'libs')
69 files changed, 1853 insertions, 393 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 0a5a81b4fd8f..a0d6fce4c39b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -33,6 +33,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; +import android.annotation.DimenRes; import android.annotation.Nullable; import android.app.ActivityThread; import android.content.Context; @@ -61,7 +62,6 @@ import android.window.TaskFragmentParentInfo; import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; -import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.window.extensions.core.util.function.Consumer; import androidx.window.extensions.embedding.SplitAttributes.SplitType; @@ -341,7 +341,7 @@ class DividerPresenter implements View.OnTouchListener { applicationContext.getResources().getDisplayMetrics()); } - private static int getDimensionDp(@IdRes int resId) { + private static int getDimensionDp(@DimenRes int resId) { final Context context = ActivityThread.currentActivityThread().getApplication(); final int px = context.getResources().getDimensionPixelSize(resId); return (int) TypedValue.convertPixelsToDimension( @@ -431,6 +431,9 @@ class DividerPresenter implements View.OnTouchListener { return null; } int widthDp = dividerAttributes.getWidthDp(); + float minRatio = dividerAttributes.getPrimaryMinRatio(); + float maxRatio = dividerAttributes.getPrimaryMaxRatio(); + if (widthDp == WIDTH_SYSTEM_DEFAULT) { widthDp = DEFAULT_DIVIDER_WIDTH_DP; } @@ -439,16 +442,14 @@ class DividerPresenter implements View.OnTouchListener { // Draggable divider width must be larger than the drag handle size. widthDp = Math.max(widthDp, getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width)); - } - - float minRatio = dividerAttributes.getPrimaryMinRatio(); - if (minRatio == RATIO_SYSTEM_DEFAULT) { - minRatio = DEFAULT_MIN_RATIO; - } - float maxRatio = dividerAttributes.getPrimaryMaxRatio(); - if (maxRatio == RATIO_SYSTEM_DEFAULT) { - maxRatio = DEFAULT_MAX_RATIO; + // Update minRatio and maxRatio only when it is a draggable divider. + if (minRatio == RATIO_SYSTEM_DEFAULT) { + minRatio = DEFAULT_MIN_RATIO; + } + if (maxRatio == RATIO_SYSTEM_DEFAULT) { + maxRatio = DEFAULT_MAX_RATIO; + } } return new DividerAttributes.Builder(dividerAttributes) diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 32f2d67888ae..a23a47416fb0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -354,14 +354,14 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean isolatedNav) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build(); + OP_TYPE_SET_ISOLATED_NAVIGATION).setBooleanValue(isolatedNav).build(); wct.addTaskFragmentOperation(fragmentToken, operation); } void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean dimOnTask) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_DIM_ON_TASK).setDimOnTask(dimOnTask).build(); + OP_TYPE_SET_DIM_ON_TASK).setBooleanValue(dimOnTask).build(); wct.addTaskFragmentOperation(fragmentToken, operation); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index de0171de4a37..8aca92e89e6b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -59,7 +59,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.concurrent.Executor; @@ -74,6 +75,9 @@ import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) public class DividerPresenterTest { @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); private static final int MOCK_TASK_ID = 1234; @@ -118,7 +122,6 @@ public class DividerPresenterTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG); when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID); @@ -263,6 +266,22 @@ public class DividerPresenterTest { } @Test + public void testSanitizeDividerAttributes_setDefaultValues_fixedDivider() { + DividerAttributes attributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_FIXED).build(); + DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes); + + assertEquals(DividerAttributes.DIVIDER_TYPE_FIXED, sanitized.getDividerType()); + assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp()); + + // The ratios should not be set for fixed divider + assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, sanitized.getPrimaryMinRatio(), + 0.0f /* delta */); + assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, sanitized.getPrimaryMaxRatio(), + 0.0f /* delta */); + } + + @Test public void testSanitizeDividerAttributes_notChangingValidValues() { DividerAttributes attributes = new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 6f37e9cb794d..76e6a0ff2c21 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -42,10 +42,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; @@ -61,6 +63,9 @@ import java.util.ArrayList; @SmallTest @RunWith(AndroidJUnit4.class) public class JetpackTaskFragmentOrganizerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private WindowContainerTransaction mTransaction; @Mock @@ -73,7 +78,6 @@ public class JetpackTaskFragmentOrganizerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback); mOrganizer.registerOrganizer(); spyOn(mOrganizer); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index b1b1984e3a70..50abdfd9a66c 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -81,7 +81,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -98,6 +99,8 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class OverlayPresentationTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); @@ -126,7 +129,6 @@ public class OverlayPresentationTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 3441c2b26ea8..8bc3a300136a 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -111,7 +111,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Collections; @@ -137,6 +138,9 @@ public class SplitControllerTest { new ComponentName("test", "placeholder")); @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); private Activity mActivity; @@ -166,7 +170,6 @@ public class SplitControllerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 62d8aa30a576..c677484f64f1 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -85,10 +85,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; @@ -106,6 +108,10 @@ import java.util.ArrayList; public class SplitPresenterTest { private Activity mActivity; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private Resources mActivityResources; @Mock @@ -119,7 +125,6 @@ public class SplitPresenterTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent) .getCurrentWindowLayoutInfo(anyInt(), any()); DeviceStateManagerFoldingFeatureProducer producer = diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index a5995a3027ac..8913b22115e9 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -42,11 +42,12 @@ import android.window.TaskFragmentParentInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.List; @@ -60,14 +61,12 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskContainerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private SplitController mController; - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - @Test public void testGetWindowingModeForSplitTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java index 379ea0c534ba..a1e9f08585f6 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java @@ -27,10 +27,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Test class for {@link TaskFragmentAnimationController}. @@ -42,13 +44,15 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentAnimationControllerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private TaskFragmentOrganizer mOrganizer; private TaskFragmentAnimationController mAnimationController; @Before public void setup() { - MockitoAnnotations.initMocks(this); mAnimationController = new TaskFragmentAnimationController(mOrganizer); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 0af41791cf4a..abfc9c861d3e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -51,10 +51,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.google.android.collect.Lists; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -71,6 +73,9 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class TaskFragmentContainerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock private SplitPresenter mPresenter; private SplitController mController; @@ -83,7 +88,6 @@ public class TaskFragmentContainerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); DeviceStateManagerFoldingFeatureProducer producer = mock(DeviceStateManagerFoldingFeatureProducer.class); WindowLayoutComponentImpl component = mock(WindowLayoutComponentImpl.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java index 459b6d2c31f9..2598dd63bbde 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java @@ -41,10 +41,12 @@ import androidx.test.filters.SmallTest; import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Test class for {@link TransactionManager}. @@ -56,6 +58,8 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class TransactionManagerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); @Mock private TaskFragmentOrganizer mOrganizer; @@ -63,7 +67,6 @@ public class TransactionManagerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mTransactionManager = new TransactionManager(mOrganizer); } diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index 0c4fd140780e..ebebd8a52c9a 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,5 +1,5 @@ xutan@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com per-file res*/*/tv_*.xml = bronger@google.com diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 89db32793b32..c371f7f62feb 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -61,10 +61,10 @@ <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Engadir de novo á pilla"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> máis"</string> - <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover á parte super. esquerda"</string> - <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover á parte superior dereita"</string> - <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover á parte infer. esquerda"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover á parte inferior dereita"</string> + <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover arriba á esquerda"</string> + <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba á dereita"</string> + <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abaixo á esquerda"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abaixo á dereita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"despregar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index e4830af1bad6..a5bd2ab5c10b 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -69,7 +69,7 @@ <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> कोल्याप्स गर्नुहोस्"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string> - <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाइयोस्"</string> + <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाउनुहोस्"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जुनसुकै बेला बबलहरू नियन्त्रण गर्नुहोस्"</string> @@ -99,7 +99,7 @@ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"यो एप तपाईंको स्क्रिनमा अझ राम्रोसँग देखियोस् भन्नाका लागि तपाईं सो एप रिस्टार्ट गर्न सक्नुहुन्छ तर तपाईंले अहिलेसम्म गरेका क्रियाकलाप वा सेभ गर्न बाँकी परिवर्तनहरू हट्न सक्छन्"</string> <string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द गर्नुहोस्"</string> <string name="letterbox_restart_restart" msgid="8529976234412442973">"रिस्टार्ट गर्नुहोस्"</string> - <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाइयोस्"</string> + <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाउनुहोस्"</string> <string name="letterbox_reachability_reposition_text" msgid="3522042240665748268">"यो एप सार्न डबल\nट्याप गर्नुहोस्"</string> <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string> <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 18d21eb6310b..1210fe8fda05 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -64,7 +64,7 @@ <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover p/ parte sup. esquerda"</string> <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover p/ parte sup. direita"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover p/ parte infer. esquerda"</string> - <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover parte inferior direita"</string> + <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover p/ parte inf. direita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expandir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"reduzir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 0799fe3b6eb2..808c212abd2f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -283,7 +283,11 @@ class CrossActivityBackAnimation @Inject constructor( scrimLayer = scrimBuilder.build() val colorComponents = floatArrayOf(0f, 0f, 0f) maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT - val scrimCrop = if (isLetterboxed) backAnimRect else closingTarget!!.localBounds + val scrimCrop = if (isLetterboxed) { + closingTarget!!.windowConfiguration.bounds + } else { + closingTarget!!.localBounds + } transaction .setColor(scrimLayer, colorComponents) .setAlpha(scrimLayer!!, maxScrimAlpha) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index a67821b7e819..0297901c8921 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -288,13 +288,16 @@ public class BadgedImageView extends ConstraintLayout { /** Sets the position of the dot and badge, animating them out and back in if requested. */ void animateDotBadgePositions(boolean onLeft) { - mOnLeft = onLeft; - - if (onLeft != getDotOnLeft() && shouldDrawDot()) { - animateDotScale(0f /* showDot */, () -> { - invalidate(); - animateDotScale(1.0f, null /* after */); - }); + if (onLeft != getDotOnLeft()) { + if (shouldDrawDot()) { + animateDotScale(0f /* showDot */, () -> { + mOnLeft = onLeft; + invalidate(); + animateDotScale(1.0f, null /* after */); + }); + } else { + mOnLeft = onLeft; + } } // TODO animate badge showBadge(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 74f087b6d8f8..4e8afccee40f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -68,6 +68,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; @@ -446,6 +447,8 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.setVisibility(GONE); } else { mTaskView = bubbleTaskView.getTaskView(); + // reset the insets that might left after TaskView is shown in BubbleBarExpandedView + mTaskView.setCaptionInsets(null); bubbleTaskView.setDelegateListener(mTaskViewListener); // set a fixed width so it is not recalculated as part of a rotation. the width will be @@ -668,6 +671,11 @@ public class BubbleExpandedView extends LinearLayout { } } + /** Sets the alpha for the pointer. */ + public void setPointerAlpha(float alpha) { + mPointerView.setAlpha(alpha); + } + /** * Get alpha from underlying {@code TaskView} if this view is for a bubble. * Or get alpha for the overflow view if this view is for overflow. @@ -698,12 +706,14 @@ public class BubbleExpandedView extends LinearLayout { } } - /** - * Sets the alpha of the background and the pointer view. - */ + /** Sets the alpha of the background. */ public void setBackgroundAlpha(float alpha) { - mPointerView.setAlpha(alpha); - setAlpha(alpha); + if (Flags.enableNewBubbleAnimations()) { + setAlpha(alpha); + } else { + mPointerView.setAlpha(alpha); + setAlpha(alpha); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 8da85d2d6abf..dcc536b5b043 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -2358,9 +2358,9 @@ public class BubbleStackView extends FrameLayout } else { index = getBubbleIndex(mExpandedBubble); } - PointF p = mPositioner.getExpandedBubbleXY(index, getState()); + PointF bubbleXY = mPositioner.getExpandedBubbleXY(index, getState()); final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, - mPositioner.showBubblesVertically() ? p.y : p.x); + mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); mExpandedViewContainer.setTranslationX(0f); mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); @@ -2371,40 +2371,42 @@ public class BubbleStackView extends FrameLayout ? mStackAnimationController.getStackPosition().y : mStackAnimationController.getStackPosition().x; final float bubbleWillBeAt = showVertically - ? p.y - : p.x; + ? bubbleXY.y + : bubbleXY.x; final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); // Wait for the path animation target to reach its end, and add a small amount of extra time // if the bubble is moving a lot horizontally. - long startDelay = 0L; + final long startDelay; // Should not happen since we lay out before expanding, but just in case... if (getWidth() > 0) { startDelay = (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f + (distanceAnimated / getWidth()) * 30); + } else { + startDelay = 0L; } // Set the pivot point for the scale, so the expanded view animates out from the bubble. if (showVertically) { float pivotX; if (mStackOnLeftOrWillBe) { - pivotX = p.x + mBubbleSize + mExpandedViewPadding; + pivotX = bubbleXY.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = p.x - mExpandedViewPadding; + pivotX = bubbleXY.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, pivotX, - p.y + mBubbleSize / 2f); + bubbleXY.y + mBubbleSize / 2f); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - p.x + mBubbleSize / 2f, - p.y + mBubbleSize + mExpandedViewPadding); + bubbleXY.x + mBubbleSize / 2f, + bubbleXY.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index b87c2f6ebad5..7ceaaea3962f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -125,6 +125,7 @@ public class PipBoundsState { private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); + private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -297,7 +298,12 @@ public class PipBoundsState { /** Set the PIP aspect ratio. */ public void setAspectRatio(float aspectRatio) { - mAspectRatio = aspectRatio; + if (Float.compare(mAspectRatio, aspectRatio) != 0) { + mAspectRatio = aspectRatio; + for (Consumer<Float> callback : mOnAspectRatioChangedCallbacks) { + callback.accept(mAspectRatio); + } + } } /** Get the PIP aspect ratio. */ @@ -527,6 +533,23 @@ public class PipBoundsState { mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback); } + /** Adds callback to listen on aspect ratio change. */ + public void addOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (!mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.add(onAspectRatioChangedCallback); + onAspectRatioChangedCallback.accept(mAspectRatio); + } + } + + /** Removes callback to listen on aspect ratio change. */ + public void removeOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.remove(onAspectRatioChangedCallback); + } + } + public LauncherState getLauncherState() { return mLauncherState; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 73228de83c0f..6834e6d3123f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -107,6 +107,7 @@ import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.HomeTransitionObserver; +import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -675,6 +676,22 @@ public abstract class WMShellBaseModule { return new TaskViewTransitions(transitions); } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract MixedTransitionHandler optionalMixedTransitionHandler(); + + @WMSingleton + @Provides + static Optional<MixedTransitionHandler> provideMixedTransitionHandler( + @DynamicOverride Optional<MixedTransitionHandler> mixedTransitionHandler + ) { + if (mixedTransitionHandler.isPresent()) { + return mixedTransitionHandler; + } + return Optional.empty(); + } + // // Keyguard transitions (optional feature) // @@ -934,6 +951,7 @@ public abstract class WMShellBaseModule { Optional<OneHandedController> oneHandedControllerOptional, Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, Optional<ActivityEmbeddingController> activityEmbeddingOptional, + Optional<MixedTransitionHandler> mixedTransitionHandler, Transitions transitions, StartingWindowController startingWindow, ProtoLogController protoLogController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1408eadf544e..b574b8159307 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -29,6 +29,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -59,6 +60,7 @@ import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; @@ -85,6 +87,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.HomeTransitionObserver; +import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -371,8 +374,9 @@ public abstract class WMShellModule { // @WMSingleton + @DynamicOverride @Provides - static DefaultMixedHandler provideDefaultMixedHandler( + static MixedTransitionHandler provideMixedTransitionHandler( ShellInit shellInit, Optional<SplitScreenController> splitScreenOptional, @Nullable PipTransitionController pipTransitionController, @@ -517,23 +521,39 @@ public abstract class WMShellModule { LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor - ) { + @ShellMainThread ShellExecutor mainExecutor, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, mainExecutor); + recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter); + } + + @WMSingleton + @Provides + static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter( + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + ShellTaskOrganizer shellTaskOrganizer) { + if (!DesktopModeStatus.isEnabled() || !Flags.enableDesktopWindowingTaskLimit()) { + return Optional.empty(); + } + return Optional.of( + new DesktopTasksLimiter( + transitions, desktopModeTaskRepository, shellTaskOrganizer)); } + @WMSingleton @Provides static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler( Context context, Transitions transitions, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DragToDesktopTransitionHandler(context, transitions, rootTaskDisplayAreaOrganizer); } @@ -541,7 +561,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( - Transitions transitions) { + Transitions transitions, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new EnterDesktopTaskTransitionHandler(transitions); } @@ -636,7 +657,6 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - DefaultMixedHandler defaultMixedHandler, Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 32c22c01a828..fcddcad3a949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -77,6 +77,22 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** + * Default value for {@code MAX_TASK_LIMIT}. + */ + @VisibleForTesting + public static final int DEFAULT_MAX_TASK_LIMIT = 4; + + // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode + /** + * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time. + * + * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen + * recording window, or Bluetooth pairing window). + */ + private static final int MAX_TASK_LIMIT = SystemProperties.getInt( + "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT); + + /** * Return {@code true} if desktop windowing is enabled */ public static boolean isEnabled() { @@ -124,6 +140,13 @@ public class DesktopModeStatus { } /** + * Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time. + */ + static int getMaxTaskLimit() { + return MAX_TASK_LIMIT; + } + + /** * Return {@code true} if the current device supports desktop mode. */ @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 50cea01fa281..2d508b2e6e3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -47,6 +47,7 @@ class DesktopModeTaskRepository { */ val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), + val minimizedTasks: ArraySet<Int> = ArraySet(), var stashed: Boolean = false ) @@ -202,6 +203,13 @@ class DesktopModeTaskRepository { } } + /** Return whether the given Task is minimized. */ + fun isMinimizedTask(taskId: Int): Boolean { + return displayData.valueIterator().asSequence().any { data -> + data.minimizedTasks.contains(taskId) + } + } + /** * Check if a task with the given [taskId] is the only active task on its display */ @@ -219,6 +227,25 @@ class DesktopModeTaskRepository { } /** + * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks + * are visible. + */ + fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 + + /** + * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, + * ordered from front to back. + */ + fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> { + val activeTasks = getActiveTasks(displayId) + val allTasksInZOrder = getFreeformTasksInZOrder() + return activeTasks + // Don't show already minimized Tasks + .filter { taskId -> !isMinimizedTask(taskId) } + .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) } + } + + /** * Get a list of freeform tasks, ordered from top-bottom (top at index 0). */ // TODO(b/278084491): pass in display id @@ -255,6 +282,7 @@ class DesktopModeTaskRepository { val prevCount = getVisibleTaskCount(displayId) if (visible) { displayData.getOrCreate(displayId).visibleTasks.add(taskId) + unminimizeTask(displayId, taskId) } else { displayData[displayId]?.visibleTasks?.remove(taskId) } @@ -312,6 +340,24 @@ class DesktopModeTaskRepository { freeformTasksInZOrder.add(0, taskId) } + /** Mark a Task as minimized. */ + fun minimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: minimize Task: display=%d, task=%d", + displayId, taskId) + displayData.getOrCreate(displayId).minimizedTasks.add(taskId) + } + + /** Mark a Task as non-minimized. */ + fun unminimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d", + displayId, taskId) + displayData[displayId]?.minimizedTasks?.remove(taskId) + } + /** * Remove the task from the ordered list. */ @@ -325,7 +371,7 @@ class DesktopModeTaskRepository { boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() + "DesktopTaskRepo: remaining freeform tasks: %s", freeformTasksInZOrder.toDumpString(), ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 068661a6a666..0a9e5d0d5345 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -88,6 +88,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import com.android.wm.shell.windowdecor.extension.isFreeform import com.android.wm.shell.windowdecor.extension.isFullscreen import java.io.PrintWriter +import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer @@ -113,7 +114,8 @@ class DesktopTasksController( private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, - @ShellMainThread private val mainExecutor: ShellExecutor + @ShellMainThread private val mainExecutor: ShellExecutor, + private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, DragAndDropController.DragAndDropListener { @@ -341,11 +343,13 @@ class DesktopTasksController( ) exitSplitIfApplicable(wct, task) // Bring other apps to front first - bringDesktopAppsToFront(task.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) addMoveToDesktopChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.moveToDesktop(wct) + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -382,10 +386,14 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - bringDesktopAppsToFront(taskInfo.displayId, wct) + moveHomeTaskToFront(wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask( + taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) wct.setBounds(taskInfo.token, freeformBounds) - dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + transition?.let { addPendingMinimizeTransition(it, taskToMinimize) } } /** @@ -507,8 +515,10 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -688,9 +698,20 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } - private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") - val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) + private fun bringDesktopAppsToFrontBeforeShowingNewTask( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int + ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) + + private fun bringDesktopAppsToFront( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int? = null + ): RunningTaskInfo? { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", + newTaskIdInFront ?: "null") if (Flags.enableDesktopWindowingWallpaperActivity()) { // Add translucent wallpaper activity to show the wallpaper underneath @@ -700,13 +721,21 @@ class DesktopTasksController( moveHomeTaskToFront(wct) } - // Then move other tasks on top of it - val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() - activeTasks - // Sort descending as the top task is at index 0. It should be ordered to top last - .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } - .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } - .forEach { task -> wct.reorder(task.token, true /* onTop */) } + val nonMinimizedTasksOrderedFrontToBack = + desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId) + // If we're adding a new Task we might need to minimize an old one + val taskToMinimize: RunningTaskInfo? = + if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { + desktopTasksLimiter.get().getTaskToMinimizeIfNeeded( + nonMinimizedTasksOrderedFrontToBack, newTaskIdInFront) + } else { null } + nonMinimizedTasksOrderedFrontToBack + // If there is a Task to minimize, let it stay behind the Home Task + .filter { taskId -> taskId != taskToMinimize?.taskId } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .reversed() // Start from the back so the front task is brought forward last + .forEach { task -> wct.reorder(task.token, true /* onTop */) } + return taskToMinimize } private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { @@ -824,13 +853,13 @@ class DesktopTasksController( when { request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) // If display has tasks stashed, handle as stashed launch - task.isStashed -> handleStashedTaskLaunch(task) + task.isStashed -> handleStashedTaskLaunch(task, transition) // Check if the task has a top transparent activity shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task) // Check if fullscreen task should be updated - task.isFullscreen -> handleFullscreenTaskLaunch(task) + task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) // Check if freeform task should be updated - task.isFreeform -> handleFreeformTaskLaunch(task) + task.isFreeform -> handleFreeformTaskLaunch(task, transition) else -> { null } @@ -878,10 +907,12 @@ class DesktopTasksController( } ?: false } - private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleFreeformTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch freeform task to fullscreen oon transition" + @@ -892,13 +923,23 @@ class DesktopTasksController( addMoveToFullscreenChanges(wct, task) } } + // Desktop Mode is showing and we're launching a new Task - we might need to minimize + // a Task. + val wct = WindowContainerTransaction() + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + if (taskToMinimize != null) { + addPendingMinimizeTransition(transition, taskToMinimize) + return wct + } return null } - private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleFullscreenTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch fullscreen task to freeform on transition" + @@ -907,21 +948,30 @@ class DesktopTasksController( ) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) + // Desktop Mode is already showing and we're launching a new Task - we might need to + // minimize another Task. + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + addPendingMinimizeTransition(transition, taskToMinimize) } } return null } - private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { + private fun handleStashedTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: launch apps with stashed on transition taskId=%d", task.taskId ) val wct = WindowContainerTransaction() - bringDesktopAppsToFront(task.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) addMoveToDesktopChanges(wct, task) desktopModeTaskRepository.setStashed(task.displayId, false) + addPendingMinimizeTransition(transition, taskToMinimize) return wct } @@ -1002,6 +1052,28 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ + private fun addAndGetMinimizeChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newTaskInfo: RunningTaskInfo + ): RunningTaskInfo? { + if (!desktopTasksLimiter.isPresent) return null + return desktopTasksLimiter.get().addAndGetMinimizeTaskChangesIfNeeded( + displayId, wct, newTaskInfo) + } + + private fun addPendingMinimizeTransition( + transition: IBinder, + taskToMinimize: RunningTaskInfo? + ) { + if (taskToMinimize == null) return + desktopTasksLimiter.ifPresent { + it.addPendingMinimizeChange( + transition, taskToMinimize.displayId, taskToMinimize.taskId) + } + } + /** Enter split by using the focused desktop task in given `displayId`. */ fun enterSplit( displayId: Int, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt new file mode 100644 index 000000000000..3404d376fe92 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.annotation.VisibleForTesting +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionObserver +import com.android.wm.shell.util.KtProtoLog + +/** + * Limits the number of tasks shown in Desktop Mode. + * + * This class should only be used if + * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true. + */ +class DesktopTasksLimiter ( + transitions: Transitions, + private val taskRepository: DesktopModeTaskRepository, + private val shellTaskOrganizer: ShellTaskOrganizer, +) { + private val minimizeTransitionObserver = MinimizeTransitionObserver() + + init { + transitions.registerObserver(minimizeTransitionObserver) + } + + private data class TaskDetails (val displayId: Int, val taskId: Int) + + // TODO(b/333018485): replace this observer when implementing the minimize-animation + private inner class MinimizeTransitionObserver : TransitionObserver { + private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + + fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { + mPendingTransitionTokensAndTasks[transition] = taskDetails + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return + + if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return + + if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: task %d is not reordered to back nor invis", + taskToMinimize.taskId) + return + } + this@DesktopTasksLimiter.markTaskMinimized( + taskToMinimize.displayId, taskToMinimize.taskId) + } + + /** + * Returns whether the given Task is being reordered to the back in the given transition, or + * is already invisible. + * + * <p> This check can be used to double-check that a task was indeed minimized before + * marking it as such. + */ + private fun isTaskReorderedToBackOrInvisible( + info: TransitionInfo, + taskDetails: TaskDetails + ): Boolean { + val taskChange = info.changes.find { change -> + change.taskInfo?.taskId == taskDetails.taskId } + if (taskChange == null) { + return !taskRepository.isVisibleTask(taskDetails.taskId) + } + return taskChange.mode == TRANSIT_TO_BACK + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + mPendingTransitionTokensAndTasks[playing] = taskToTransfer + } + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: transition %s finished", transition) + mPendingTransitionTokensAndTasks.remove(transition) + } + } + + /** + * Mark a task as minimized, this should only be done after the corresponding transition has + * finished so we don't minimize the task if the transition fails. + */ + private fun markTaskMinimized(displayId: Int, taskId: Int) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: marking %d as minimized", taskId) + taskRepository.minimizeTask(displayId, taskId) + } + + /** + * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task + * limit. + * + * @param transition the transition that the minimize-transition will be appended to, or null if + * the transition will be started later. + * @return the ID of the minimized task, or null if no task is being minimized. + */ + fun addAndGetMinimizeTaskChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newFrontTaskInfo: RunningTaskInfo, + ): RunningTaskInfo? { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", + newFrontTaskInfo.taskId) + val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( + taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId), + newFrontTaskInfo.taskId) + val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) + if (taskToMinimize != null) { + wct.reorder(taskToMinimize.token, false /* onTop */) + return taskToMinimize + } + return null + } + + /** + * Add a pending minimize transition change, to update the list of minimized apps once the + * transition goes through. + */ + fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) { + minimizeTransitionObserver.addPendingTransitionToken( + transition, TaskDetails(displayId, taskId)) + } + + /** + * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop + * Mode. + */ + fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit() + + /** + * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and + * 2. a new task placed in front of all the others. + */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int>, + newTaskIdInFront: Int + ): RunningTaskInfo? { + return getTaskToMinimizeIfNeeded( + createOrderedTaskListWithGivenTaskInFront( + visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront)) + } + + /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int> + ): RunningTaskInfo? { + if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: no need to minimize; tasks below limit") + // No need to minimize anything + return null + } + val taskToMinimize = + shellTaskOrganizer.getRunningTaskInfo( + visibleFreeformTaskIdsOrderedFrontToBack.last()) + if (taskToMinimize == null) { + KtProtoLog.e( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: taskToMinimize == null") + return null + } + return taskToMinimize + } + + private fun createOrderedTaskListWithGivenTaskInFront( + existingTaskIdsOrderedFrontToBack: List<Int>, + newTaskId: Int + ): List<Int> { + return listOf(newTaskId) + + existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId } + } + + @VisibleForTesting + fun getTransitionObserver(): TransitionObserver { + return minimizeTransitionObserver + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 0061d03af8e9..e341f2d4d4b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -150,20 +150,20 @@ class DragToDesktopTransitionHandler( * windowing mode changes to the dragged task. This is called when the dragged task is released * inside the desktop drop zone. */ - fun finishDragToDesktopTransition(wct: WindowContainerTransaction) { + fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { // Don't attempt to finish a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully // started. - return + return null } if (requireTransitionState().startAborted) { // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. clearState() - return + return null } - transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 79bb5408df82..74b8f831cdc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -78,10 +78,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition /** * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP * @param wct WindowContainerTransaction for transition + * @return the token representing the started transition */ - public void moveToDesktop(@NonNull WindowContainerTransaction wct) { + public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) { final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); mPendingTransitionTokens.add(token); + return token; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index f2bdcae31956..6fea2036dbd1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -95,6 +95,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, @@ -116,6 +117,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (repository.removeActiveTask(taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Removing active freeform task: #%d", taskInfo.taskId); @@ -162,6 +164,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS index ec09827fa4d1..afddfab99a2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS @@ -1,3 +1,2 @@ # WM shell sub-module pip owner hwwang@google.com -mateuszc@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4c477373c32c..eb845db409e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -743,11 +743,6 @@ public class PipAnimationController { .alpha(tx, leash, 1f) .round(tx, leash, shouldApplyCornerRadius()) .shadow(tx, leash, shouldApplyShadowRadius()); - // TODO(b/178632364): this is a work around for the black background when - // entering PiP in button navigation mode. - if (isInPipDirection(direction)) { - tx.setWindowCrop(leash, getStartValue()); - } tx.show(leash); tx.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 57cf99211b6e..e885262658f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -597,6 +597,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } + if (mPipTransitionState.isEnteringPip() + && !mPipTransitionState.getInSwipePipToHomeTransition()) { + // If we are still entering PiP with Shell playing enter animation, jump-cut to + // the end of the enter animation and reschedule exitPip to run after enter-PiP + // has finished its transition and allowed the client to draw in PiP mode. + mPipTransitionController.end(() -> { + exitPip(animationDurationMs, requestEnterSplit); + }); + return; + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); final WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index d60f5a631044..fdde3ee01264 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -43,7 +43,6 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; -import android.animation.Animator; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.TaskInfo; @@ -348,9 +347,16 @@ public class PipTransition extends PipTransitionController { @Override public void end() { - Animator animator = mPipAnimationController.getCurrentAnimator(); - if (animator != null && animator.isRunning()) { - animator.end(); + end(null); + } + + @Override + public void end(@Nullable Runnable onTransitionEnd) { + if (mPipAnimationController.isAnimating()) { + mPipAnimationController.getCurrentAnimator().end(); + } + if (onTransitionEnd != null) { + onTransitionEnd.run(); } } @@ -818,8 +824,13 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull TaskInfo taskInfo) { startTransaction.apply(); - finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), - mPipDisplayLayoutState.getDisplayBounds()); + if (info.getChanges().isEmpty()) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "removePipImmediately is called with empty changes"); + } else { + finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), + mPipDisplayLayoutState.getDisplayBounds()); + } mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(null); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 32442f740a52..4f71a02528c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -305,6 +305,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void end() { } + /** + * End the currently-playing PiP animation. + * + * @param onTransitionEnd callback to run upon finishing the playing transition. + */ + public void end(@Nullable Runnable onTransitionEnd) { + } + /** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */ public void startHighPerfSession() {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index c1adfffce074..d8ac8e948a97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -219,6 +219,7 @@ public class PipTouchHandler { mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), this::onAccessibilityShowMenu, this::updateMovementBounds, this::animateToUnStashedState, mainExecutor); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); // TODO(b/181599115): This should really be initializes as part of the pip controller, but // until all PIP implementations derive from the controller, just initialize the touch handler diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS index 6dabb3bf6f9a..79d1793819f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS @@ -1,4 +1,3 @@ # WM shell sub-module pip owner hwwang@google.com -mateuszc@google.com gabiyev@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index cc8e3e0934e6..472003cb435f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -208,6 +208,7 @@ public class PipTouchHandler { new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mTouchState, this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, mPipPerfHintController); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); if (PipUtils.isPip2ExperimentEnabled()) { shellInit.addInitCallback(this::onInit, this); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index 35a1fa0a92f6..a85188a9e04d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -30,7 +30,6 @@ import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; -import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -121,6 +120,11 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mTaskViewTaskController.isUsingShellTransitions()) { + // No need for additional work as it is already taken care of during + // prepareOpenAnimation(). + return; + } onLocationChanged(); if (taskInfo.taskDescription != null) { final int bgColor = taskInfo.taskDescription.getBackgroundColor(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 196e04edbb10..11aa402aa283 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,6 +49,23 @@ import java.util.concurrent.Executor; * TaskView} to {@link TaskViewTaskController} interactions are done via direct method calls. * * The reverse communication is done via the {@link TaskViewBase} interface. + * + * <ul> + * <li>The entry point for an activity based task view is {@link + * TaskViewTaskController#startActivity(PendingIntent, Intent, ActivityOptions, Rect)}</li> + * + * <li>The entry point for an activity (represented by {@link ShortcutInfo}) based task view + * is {@link TaskViewTaskController#startShortcutActivity(ShortcutInfo, ActivityOptions, Rect)} + * </li> + * + * <li>The entry point for a root-task based task view is {@link + * TaskViewTaskController#startRootTask(ActivityManager.RunningTaskInfo, SurfaceControl, + * WindowContainerTransaction)}. + * This method is special as it doesn't create a root task and instead expects that the + * launch root task is already created and started. This method just attaches the taskInfo to + * the TaskView. + * </li> + * </ul> */ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { @@ -155,8 +173,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { * <p>The owner of this container must be allowed to access the shortcut information, * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. * - * @param shortcut the shortcut used to launch the activity. - * @param options options for the activity. + * @param shortcut the shortcut used to launch the activity. + * @param options options for the activity. * @param launchBounds the bounds (window size and position) that the activity should be * launched in, in pixels and in screen coordinates. */ @@ -183,10 +201,10 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { * Launch a new activity. * * @param pendingIntent Intent used to launch an activity. - * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} - * @param options options for the activity. - * @param launchBounds the bounds (window size and position) that the activity should be - * launched in, in pixels and in screen coordinates. + * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} + * @param options options for the activity. + * @param launchBounds the bounds (window size and position) that the activity should be + * launched in, in pixels and in screen coordinates. */ public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds) { @@ -208,6 +226,35 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } } + + /** + * Attaches the given root task {@code taskInfo} in the task view. + * + * <p> Since {@link ShellTaskOrganizer#createRootTask(int, int, + * ShellTaskOrganizer.TaskListener)} does not use the shell transitions flow, this method is + * used as an entry point for an already-created root-task in the task view. + * + * @param taskInfo the task info of the root task. + * @param leash the {@link android.content.pm.ShortcutInfo.Surface} of the root task + * @param wct The Window container work that should happen as part of this set up. + */ + public void startRootTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + @Nullable WindowContainerTransaction wct) { + if (wct == null) { + wct = new WindowContainerTransaction(); + } + // This method skips the regular flow where an activity task is launched as part of a new + // transition in taskview and then transition is intercepted using the launchcookie. + // The task here is already created and running, it just needs to be reparented, resized + // and tracked correctly inside taskview. Which is done by calling + // prepareOpenAnimationInternal() and then manually enqueuing the resulting window container + // transaction. + prepareOpenAnimationInternal(true /* newTask */, mTransaction /* startTransaction */, + null /* finishTransaction */, taskInfo, leash, wct); + mTransaction.apply(); + mTaskViewTransitions.startInstantTransition(TRANSIT_CHANGE, wct); + } + private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { final Binder launchCookie = new Binder(); mShellExecutor.execute(() -> { @@ -342,7 +389,6 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { final SurfaceControl taskLeash = mTaskLeash; handleAndNotifyTaskRemoval(mTaskInfo); - // Unparent the task when this surface is destroyed mTransaction.reparent(taskLeash, null).apply(); resetTaskInfo(); } @@ -597,6 +643,15 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { @NonNull SurfaceControl.Transaction finishTransaction, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, WindowContainerTransaction wct) { + prepareOpenAnimationInternal(newTask, startTransaction, finishTransaction, taskInfo, leash, + wct); + } + + private void prepareOpenAnimationInternal(final boolean newTask, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + WindowContainerTransaction wct) { mPendingInfo = null; mTaskInfo = taskInfo; mTaskToken = mTaskInfo.token; @@ -608,10 +663,12 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { // Also reparent on finishTransaction since the finishTransaction will reparent back // to its "original" parent by default. Rect boundsOnScreen = mTaskViewBase.getCurrentBoundsOnScreen(); - finishTransaction.reparent(mTaskLeash, mSurfaceControl) - .setPosition(mTaskLeash, 0, 0) - // TODO: maybe once b/280900002 is fixed this will be unnecessary - .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height()); + if (finishTransaction != null) { + finishTransaction.reparent(mTaskLeash, mSurfaceControl) + .setPosition(mTaskLeash, 0, 0) + // TODO: maybe once b/280900002 is fixed this will be unnecessary + .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height()); + } mTaskViewTransitions.updateBoundsState(this, boundsOnScreen); mTaskViewTransitions.updateVisibilityState(this, true /* visible */); wct.setBounds(mTaskToken, boundsOnScreen); @@ -632,6 +689,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor); } + mTaskViewBase.onTaskAppeared(mTaskInfo, mTaskLeash); if (mListener != null) { final int taskId = mTaskInfo.taskId; final ComponentName baseActivity = mTaskInfo.baseActivity; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 198ec82b5f21..e6d1b4593a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -53,7 +53,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { new ArrayMap<>(); private final ArrayList<PendingTransition> mPending = new ArrayList<>(); private final Transitions mTransitions; - private final boolean[] mRegistered = new boolean[]{ false }; + private final boolean[] mRegistered = new boolean[]{false}; /** * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be @@ -122,6 +122,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { /** * Looks through the pending transitions for a closing transaction that matches the provided * `taskView`. + * * @param taskView the pending transition should be for this. */ private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) { @@ -135,8 +136,17 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } /** + * Starts a transition outside of the handler associated with {@link TaskViewTransitions}. + */ + public void startInstantTransition(@WindowManager.TransitionType int type, + WindowContainerTransaction wct) { + mTransitions.startTransition(type, wct, null); + } + + /** * Looks through the pending transitions for a opening transaction that matches the provided * `taskView`. + * * @param taskView the pending transition should be for this. */ @VisibleForTesting @@ -152,8 +162,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { /** * Looks through the pending transitions for one matching `taskView`. + * * @param taskView the pending transition should be for this. - * @param type the type of transition it's looking for + * @param type the type of transition it's looking for */ PendingTransition findPending(TaskViewTaskController taskView, int type) { for (int i = mPending.size() - 1; i >= 0; --i) { @@ -220,7 +231,24 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { startNextTransition(); } - void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + /** Starts a new transition to make the given {@code taskView} visible. */ + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + setTaskViewVisible(taskView, visible, false /* reorder */); + } + + /** + * Starts a new transition to make the given {@code taskView} visible and optionally change + * the task order. + * + * @param taskView the task view which the visibility is being changed for + * @param visible the new visibility of the task view + * @param reorder whether to reorder the task or not. If this is {@code true}, the task will be + * reordered as per the given {@code visible}. For {@code visible = true}, task + * will be reordered to top. For {@code visible = false}, task will be reordered + * to the bottom + */ + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible, + boolean reorder) { if (mTaskViews.get(taskView) == null) return; if (mTaskViews.get(taskView).mVisible == visible) return; if (taskView.getTaskInfo() == null) { @@ -231,6 +259,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */); wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds); + if (reorder) { + wct.reorder(taskView.getTaskInfo().token, visible /* onTop */); + } PendingTransition pending = new PendingTransition( visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); mPending.add(pending); @@ -238,6 +269,22 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // visibility is reported in transition. } + /** Starts a new transition to reorder the given {@code taskView}'s task. */ + public void reorderTaskViewTask(TaskViewTaskController taskView, boolean onTop) { + if (mTaskViews.get(taskView) == null) return; + if (taskView.getTaskInfo() == null) { + // Nothing to update, task is not yet available + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(taskView.getTaskInfo().token, onTop /* onTop */); + PendingTransition pending = new PendingTransition( + onTop ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); + mPending.add(pending); + startNextTransition(); + // visibility is reported in transition. + } + void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) { TaskViewRequestedState state = mTaskViews.get(taskView); if (state == null) return; @@ -380,7 +427,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) - .setPosition(chg.getLeash(), 0, 0); + .setPosition(chg.getLeash(), 0, 0); changesHandled++; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 8746b8c8d55c..4bc0dc00175b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -61,9 +61,10 @@ import java.util.function.Consumer; /** * A handler for dealing with transitions involving multiple other handlers. For example: an - * activity in split-screen going into PiP. + * activity in split-screen going into PiP. Note this is provided as a handset-specific + * implementation of {@code MixedTransitionHandler}. */ -public class DefaultMixedHandler implements Transitions.TransitionHandler, +public class DefaultMixedHandler implements MixedTransitionHandler, RecentsTransitionHandler.RecentsMixedHandler { private final Transitions mPlayer; @@ -116,7 +117,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, final IBinder mTransition; protected final Transitions mPlayer; - protected final DefaultMixedHandler mMixedHandler; + protected final MixedTransitionHandler mMixedHandler; protected final PipTransitionController mPipHandler; protected final StageCoordinator mSplitHandler; protected final KeyguardTransitionHandler mKeyguardHandler; @@ -142,7 +143,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, int mInFlightSubAnimations = 0; MixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) { mType = type; mTransition = transition; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index e9cd73b0df5e..b028bd65b438 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -41,7 +41,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { private final ActivityEmbeddingController mActivityEmbeddingController; DefaultMixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, UnfoldTransitionHandler unfoldHandler, ActivityEmbeddingController activityEmbeddingController) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java new file mode 100644 index 000000000000..ff429fb12c94 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +/** + * Interface for a {@link Transitions.TransitionHandler} that can take the subset of transitions + * that it handles and further decompose those transitions into sub-transitions which can be + * independently delegated to separate handlers. + */ +public interface MixedTransitionHandler extends Transitions.TransitionHandler { + + // TODO(b/335685449) this currently exists purely as a marker interface for use in form-factor + // specific/sysui dagger modules. Going forward, we should define this in a meaningful + // way so as to provide a clear basis for expectations/behaviours associated with mixed + // transitions and their default handlers. + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java index 0974cd13f249..ffc0b76b131d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java @@ -44,7 +44,7 @@ public class MixedTransitionHelper { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler, + @NonNull Transitions player, @NonNull MixedTransitionHandler mixedHandler, @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering PIP while Split-Screen is foreground."); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 5b402a5a7d53..d6e64cfaf4d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -43,7 +43,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { private final DesktopTasksController mDesktopTasksController; RecentsMixedTransition(int type, IBinder transition, Transitions player, - DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, RecentsTransitionHandler recentsHandler, DesktopTasksController desktopTasksController) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java index fa331af267fa..ed4ae05cb2c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java @@ -16,7 +16,10 @@ package com.android.wm.shell.transition.tracing; -import android.internal.perfetto.protos.PerfettoTrace; +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping; +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings; +import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition; +import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket; import android.os.SystemClock; import android.os.Trace; import android.tracing.perfetto.DataSourceParams; @@ -72,11 +75,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { final int handlerId = getHandlerId(handler); final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, transitionId); - os.write(PerfettoTrace.ShellTransition.DISPATCH_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, transitionId); + os.write(ShellTransition.DISPATCH_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.HANDLER, handlerId); + os.write(ShellTransition.HANDLER, handlerId); os.end(token); }); } @@ -117,11 +120,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); - os.write(PerfettoTrace.ShellTransition.MERGE_REQUEST_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, mergeRequestedTransitionId); + os.write(ShellTransition.MERGE_REQUEST_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.write(ShellTransition.MERGE_TARGET, playingTransitionId); os.end(token); }); } @@ -149,11 +152,11 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogMerged(int mergeRequestedTransitionId, int playingTransitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); - os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, mergeRequestedTransitionId); + os.write(ShellTransition.MERGE_TIME_NS, SystemClock.elapsedRealtimeNanos()); - os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.write(ShellTransition.MERGE_TARGET, playingTransitionId); os.end(token); }); } @@ -180,9 +183,9 @@ public class PerfettoTransitionTracer implements TransitionTracer { private void doLogAborted(int transitionId) { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); - os.write(PerfettoTrace.ShellTransition.ID, transitionId); - os.write(PerfettoTrace.ShellTransition.SHELL_ABORT_TIME_NS, + final long token = os.start(TracePacket.SHELL_TRANSITION); + os.write(ShellTransition.ID, transitionId); + os.write(ShellTransition.SHELL_ABORT_TIME_NS, SystemClock.elapsedRealtimeNanos()); os.end(token); }); @@ -196,14 +199,14 @@ public class PerfettoTransitionTracer implements TransitionTracer { mDataSource.trace(ctx -> { final ProtoOutputStream os = ctx.newTracePacket(); - final long mappingsToken = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS); + final long mappingsToken = os.start(TracePacket.SHELL_HANDLER_MAPPINGS); for (Map.Entry<String, Integer> entry : mHandlerMapping.entrySet()) { final String handler = entry.getKey(); final int handlerId = entry.getValue(); - final long mappingEntryToken = os.start(PerfettoTrace.ShellHandlerMappings.MAPPING); - os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerId); - os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler); + final long mappingEntryToken = os.start(ShellHandlerMappings.MAPPING); + os.write(ShellHandlerMapping.ID, handlerId); + os.write(ShellHandlerMapping.NAME, handler); os.end(mappingEntryToken); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 777ab9c17218..6a9d17fef928 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -1182,7 +1182,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final boolean inImmersiveMode = !source.isVisible(); // Calls WindowDecoration#relayout if decoration visibility needs to be updated if (inImmersiveMode != mInImmersiveMode) { - decor.relayout(decor.mTaskInfo); + if (Flags.enableDesktopWindowingImmersiveHandleHiding()) { + decor.relayout(decor.mTaskInfo); + } mInImmersiveMode = inImmersiveMode; } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt new file mode 100644 index 000000000000..5563bb9fa934 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt new file mode 100644 index 000000000000..3d16d2219c78 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt new file mode 100644 index 000000000000..75dfeba3e662 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType + +class DesktopModeFlickerScenarios { + companion object { + val END_DRAG_TO_DESKTOP = + FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP + } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd( + Components.DESKTOP_MODE_APP + ) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { it.type == TransitionType.CLOSE } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions.findLast { it.type == TransitionType.CLOSE } + return if (lastTransition != null) listOf(lastTransition) + else emptyList() + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + LauncherWindowMovesToTop() + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt index 4c781d36acf6..9dfafe958b0b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt @@ -17,58 +17,28 @@ package com.android.wm.shell.flicker.service.desktopmode.flicker import android.tools.Rotation -import android.tools.flicker.AssertionInvocationGroup import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios import android.tools.flicker.annotation.FlickerConfigProvider -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd -import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfig -import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.FlickerServiceConfig -import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components -import android.tools.flicker.extractors.ITransitionMatcher -import android.tools.flicker.extractors.ShellTransitionScenarioExtractor import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import android.tools.traces.wm.Transition -import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) { - @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = - super.enterDesktopWithDrag() + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() companion object { - private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( - scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), - extractor = ShellTransitionScenarioExtractor( - transitionMatcher = object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} - } - }), - assertions = AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) - ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) @JvmStatic @FlickerConfigProvider fun flickerConfigProvider(): FlickerConfig = - FlickerConfig() - .use(FlickerServiceConfig.DEFAULT) - .use(END_DRAG_TO_DESKTOP) + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt index d99d875fb126..1c7d6237eb8a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt @@ -17,58 +17,27 @@ package com.android.wm.shell.flicker.service.desktopmode.flicker import android.tools.Rotation -import android.tools.flicker.AssertionInvocationGroup import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios import android.tools.flicker.annotation.FlickerConfigProvider -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd -import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfig -import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.FlickerServiceConfig -import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components -import android.tools.flicker.extractors.ITransitionMatcher -import android.tools.flicker.extractors.ShellTransitionScenarioExtractor import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import android.tools.traces.wm.Transition -import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) { - @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = - super.enterDesktopWithDrag() + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() companion object { - private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( - scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), - extractor = ShellTransitionScenarioExtractor( - transitionMatcher = object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} - } - }), - assertions = AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) - ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - @JvmStatic @FlickerConfigProvider fun flickerConfigProvider(): FlickerConfig = - FlickerConfig() - .use(FlickerServiceConfig.DEFAULT) - .use(END_DRAG_TO_DESKTOP) + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt new file mode 100644 index 000000000000..0c2b5015840d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Base Test Class") +abstract class CloseAllAppsWithAppHeaderExit +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + + + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode()) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + nonResizeableApp.launchViaIntent(wmHelper) + } + + @Test + open fun closeAllAppsInDesktop() { + nonResizeableApp.closeDesktopApp(wmHelper, device) + mailApp.closeDesktopApp(wmHelper, device) + testApp.closeDesktopApp(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt index 0403b4f64faf..9e9998ef7c2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt @@ -23,15 +23,18 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags import com.android.wm.shell.flicker.service.common.Utils -import com.android.wm.shell.flicker.utils.DesktopModeUtils import org.junit.After +import org.junit.Assume import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test + @Ignore("Base Test Class") abstract class EnterDesktopWithDrag @JvmOverloads @@ -41,19 +44,20 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val tapl = LauncherInstrumentation() private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) - private val testApp = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @Before fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode()) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) } @Test open fun enterDesktopWithDrag() { - DesktopModeUtils.enterDesktopWithDrag(wmHelper, device, testApp) + testApp.enterDesktopWithDrag(wmHelper, device) } @After diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt deleted file mode 100644 index 345bc5ebb20e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.utils - -import android.tools.device.apphelpers.StandardAppHelper -import android.tools.helpers.SYSTEMUI_PACKAGE -import android.tools.traces.component.IComponentMatcher -import android.tools.traces.parsers.WindowManagerStateHelper -import android.tools.traces.wm.WindowingMode -import androidx.test.uiautomator.By -import androidx.test.uiautomator.BySelector -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until - -/** - * Provides a collection of utility functions for desktop mode testing. - */ -object DesktopModeUtils { - private const val TIMEOUT_MS = 3_000L - private const val CAPTION = "desktop_mode_caption" - private const val CAPTION_HANDLE = "caption_handle" - private const val MAXIMIZE_BUTTON = "maximize_button_view" - - private val captionFullscreen: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION) - private val captionHandle: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION_HANDLE) - private val maximizeButton: BySelector - get() = By.res(SYSTEMUI_PACKAGE, MAXIMIZE_BUTTON) - - /** - * Wait for an app moved to desktop to finish its transition. - */ - private fun waitForAppToMoveToDesktop( - wmHelper: WindowManagerStateHelper, - currentApp: IComponentMatcher, - ) { - wmHelper - .StateSyncBuilder() - .withWindowSurfaceAppeared(currentApp) - .withFreeformApp(currentApp) - .withAppTransitionIdle() - .waitForAndVerify() - } - - /** - * Click maximise button on the app header for the given app. - */ - fun maximiseDesktopApp( - wmHelper: WindowManagerStateHelper, - device: UiDevice, - currentApp: StandardAppHelper - ) { - if (wmHelper.getWindow(currentApp)?.windowingMode - != WindowingMode.WINDOWING_MODE_FREEFORM.value) - error("expected a freeform window to maximise but window is not in freefrom mode") - - val maximizeButton = - device.wait(Until.findObject(maximizeButton), TIMEOUT_MS) - ?: error("Unable to find view $maximizeButton\n") - maximizeButton.click() - } - - /** - * Move an app to Desktop by dragging the app handle at the top. - */ - fun enterDesktopWithDrag( - wmHelper: WindowManagerStateHelper, - device: UiDevice, - currentApp: StandardAppHelper, - ) { - currentApp.launchViaIntent(wmHelper) - dragToDesktop(wmHelper, currentApp, device) - waitForAppToMoveToDesktop(wmHelper, currentApp) - } - - private fun dragToDesktop( - wmHelper: WindowManagerStateHelper, - currentApp: StandardAppHelper, - device: UiDevice - ) { - val windowRect = wmHelper.getWindowRegion(currentApp).bounds - val startX = windowRect.centerX() - - // Start dragging a little under the top to prevent dragging the notification shade. - val startY = 10 - - val displayRect = - wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect - ?: throw IllegalStateException("Default display is null") - - // The position we want to drag to - val endY = displayRect.centerY() / 2 - - // drag the window to move to desktop - device.drag(startX, startY, startX, endY, 100) - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index b2b54acf4585..dca7be12fffc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -483,6 +483,102 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } + @Test + fun minimizeTaskNotCalled_noTasksMinimized() { + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + } + + @Test + fun minimizeTask_onlyThatTaskIsMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isTrue() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun unminimizeTask_taskNoLongerMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + repo.unminimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun unminimizeTask_nonExistentTask_doesntCrash() { + repo.unminimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + + @Test + fun updateVisibleFreeformTasks_toVisible_taskIsUnminimized() { + repo.minimizeTask(displayId = 10, taskId = 2) + + repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true) + + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun isDesktopModeShowing_noActiveTasks_returnsFalse() { + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 2, 3)) + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + repo.minimizeTask(displayId = 0, taskId = 2) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 3)) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 64f604119a8b..ad4b720facd7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -105,6 +105,7 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.quality.Strictness +import java.util.Optional import org.mockito.Mockito.`when` as whenever /** @@ -145,6 +146,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private val shellExecutor = TestShellExecutor() @@ -160,9 +162,12 @@ class DesktopTasksControllerTest : ShellTestCase() { shellInit = Mockito.spy(ShellInit(testExecutor)) desktopModeTaskRepository = DesktopModeTaskRepository() + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(enterDesktopTransitionHandler.moveToDesktop(any())).thenAnswer { Binder() } whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) @@ -203,7 +208,8 @@ class DesktopTasksControllerTest : ShellTestCase() { launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, - shellExecutor + shellExecutor, + Optional.of(desktopTasksLimiter), ) } @@ -409,6 +415,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun showDesktopApps_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct( + type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Reorder home and freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 0, homeTask, toTop = true) + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -606,6 +631,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val homeTask = setUpHomeTask() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + + controller.moveToDesktop(newTask) + + val wct = getLatestMoveToDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + for (i in 1..<taskLimit) { // Skipping freeformTasks[0] + wct.assertReorderAt(index = i, task = freeformTasks[i]) + } + wct.assertReorderAt(taskLimit, newTask) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -659,6 +702,20 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + setUpHomeTask() + val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } + + controller.moveTaskToFront(freeformTasks[0]) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = true) + wct.assertReorderAt(1, freeformTasks[1], toTop = false) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -777,6 +834,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + wct!!.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -841,6 +930,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom + } + + @Test fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1352,11 +1457,16 @@ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { .isGreaterThan(index) } -private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { +private fun WindowContainerTransaction.assertReorderAt( + index: Int, + task: RunningTaskInfo, + toTop: Boolean? = null +) { assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) + toTop?.let { assertThat(op.toTop).isEqualTo(it) } } private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt new file mode 100644 index 000000000000..38ea03471b07 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.os.Binder +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.StubTransaction +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.quality.Strictness + + +/** + * Test class for {@link DesktopTasksLimiter} + * + * Usage: atest WMShellUnitTests:DesktopTasksLimiterTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTasksLimiterTest : ShellTestCase() { + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var transitions: Transitions + + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var desktopTasksLimiter: DesktopTasksLimiter + private lateinit var desktopTaskRepo: DesktopModeTaskRepository + + @Before + fun setUp() { + mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java).startMocking() + `when`(DesktopModeStatus.isEnabled()).thenReturn(true) + + desktopTaskRepo = DesktopModeTaskRepository() + + desktopTasksLimiter = DesktopTasksLimiter( + transitions, desktopTaskRepo, shellTaskOrganizer) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + // Currently, the task limit can be overridden through an adb flag. This test ensures the limit + // hasn't been overridden. + @Test + fun getMaxTaskLimit_isSameAsConstant() { + assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo( + DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT) + } + + @Test + fun addPendingMinimizeTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.addPendingMinimizeChange(Binder(), displayId = 1, taskId = task.taskId) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_noPendingTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + Binder() /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() { + val pendingTransition = Binder() + val taskTransition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + taskTransition /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskVisible(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { + val mergedTransition = Binder() + val newTransition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + desktopTasksLimiter.getTransitionObserver().onTransitionMerged( + mergedTransition, newTransition) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + newTransition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + (1..<taskLimit).forEach { _ -> setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + // The following list will be ordered bottom -> top, as the last task is moved to top last. + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isEqualTo(tasks.first()) + assertThat(wct.hierarchyOps.size).isEqualTo(1) + assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(wct.hierarchyOps[0].toTop).isFalse() // Reorder to bottom + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = 0, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + assertThat(minimizedTask).isNull() + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit + 1).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + @Test + fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }, + newTaskIdInFront = setUpFreeformTask().taskId) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + ): RunningTaskInfo { + val task = createFreeformTask(displayId) + `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopTaskRepo.addActiveTask(displayId, task.taskId) + desktopTaskRepo.addOrMoveFreeformTaskToTop(task.taskId) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = true + ) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = false + ) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index d7c46104b6b1..0434742c571b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -44,7 +44,6 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; -import android.os.Handler; import android.os.Looper; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -498,6 +497,31 @@ public class TaskViewTest extends ShellTestCase { } @Test + public void testStartRootTask_setsBoundsAndVisibility() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + TaskViewBase taskViewBase = mock(TaskViewBase.class); + Rect bounds = new Rect(0, 0, 100, 100); + when(taskViewBase.getCurrentBoundsOnScreen()).thenReturn(bounds); + mTaskViewTaskController.setTaskViewBase(taskViewBase); + + // Surface created, but task not available so bounds / visibility isn't set + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + verify(mTaskViewTransitions, never()).updateVisibilityState( + eq(mTaskViewTaskController), eq(true)); + + // Make the task available + WindowContainerTransaction wct = mock(WindowContainerTransaction.class); + mTaskViewTaskController.startRootTask(mTaskInfo, mLeash, wct); + + // Bounds got set + verify(wct).setBounds(any(WindowContainerToken.class), eq(bounds)); + // Visibility & bounds state got set + verify(mTaskViewTransitions).updateVisibilityState(eq(mTaskViewTaskController), eq(true)); + verify(mTaskViewTransitions).updateBoundsState(eq(mTaskViewTaskController), eq(bounds)); + } + + @Test public void testTaskViewPrepareOpenAnimationSetsBoundsAndVisibility() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index fbc0db9c2850..d3e40f21db23 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -18,6 +18,7 @@ package com.android.wm.shell.taskview; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.google.common.truth.Truth.assertThat; @@ -208,6 +209,48 @@ public class TaskViewTransitionsTest extends ShellTestCase { } @Test + public void testReorderTask_movedToFrontTransaction() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, true); + // Consume the pending transaction from order change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + + // Verify it was consumed + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending2).isNull(); + } + + @Test + public void testReorderTask_movedToBackTransaction() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, false); + // Consume the pending transaction from order change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + + // Verify it was consumed + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK); + assertThat(pending2).isNull(); + } + + @Test public void test_startAnimation_setsTaskNotFound() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 8e9619dc1430..7d19f3cbf659 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -28,6 +28,9 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.os.Handler import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -97,6 +100,10 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Rule val setFlagsRule = SetFlagsRule() + @JvmField + @Rule + val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @Mock private lateinit var mockDesktopModeWindowDecorFactory: DesktopModeWindowDecoration.Factory @Mock private lateinit var mockMainHandler: Handler @@ -306,6 +313,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -326,6 +334,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutDoesNotRunWhenNonStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -346,6 +355,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testRelayoutDoesNotRunWhenNonStatusBarsInsetSourceVisibilityDoesNotChange() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp index ec304378c6c4..b4fd5d9aab3d 100644 --- a/libs/hostgraphics/HostBufferQueue.cpp +++ b/libs/hostgraphics/HostBufferQueue.cpp @@ -16,12 +16,15 @@ #include <gui/BufferQueue.h> +#include <system/window.h> + namespace android { class HostBufferQueue : public IGraphicBufferProducer, public IGraphicBufferConsumer { public: HostBufferQueue() : mWidth(0), mHeight(0) { } +// Consumer virtual status_t setConsumerIsProtected(bool isProtected) { return OK; } virtual status_t detachBuffer(int slot) { return OK; } @@ -51,6 +54,28 @@ public: virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { return OK; } virtual status_t setConsumerUsageBits(uint64_t usage) { return OK; } + +// Producer + virtual int query(int what, int* value) { + switch(what) { + case NATIVE_WINDOW_WIDTH: + *value = mWidth; + break; + case NATIVE_WINDOW_HEIGHT: + *value = mHeight; + break; + default: + *value = 0; + break; + } + return OK; + } + + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) { + *buf = mBuffer; + return OK; + } + private: sp<GraphicBuffer> mBuffer; uint32_t mWidth; diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/gui/IGraphicBufferProducer.h index a1efd0bcfa4c..1447a4fd3695 100644 --- a/libs/hostgraphics/gui/IGraphicBufferProducer.h +++ b/libs/hostgraphics/gui/IGraphicBufferProducer.h @@ -31,6 +31,10 @@ public: // Disconnect any API originally connected from the process calling disconnect. AllLocal }; + + virtual int query(int what, int* value) = 0; + + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) = 0; }; } // namespace android diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h index 36d8fba0d61a..0d810370c47c 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/gui/Surface.h @@ -17,18 +17,21 @@ #ifndef ANDROID_GUI_SURFACE_H #define ANDROID_GUI_SURFACE_H -#include <gui/IGraphicBufferProducer.h> +#include <system/window.h> #include <ui/ANativeObjectBase.h> #include <utils/RefBase.h> -#include <system/window.h> + +#include "gui/IGraphicBufferProducer.h" namespace android { class Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase> { public: - explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, - bool controlledByApp = false) { + explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp = false) + : mBufferProducer(bufferProducer) { ANativeWindow::perform = hook_perform; + ANativeWindow::dequeueBuffer = hook_dequeueBuffer; + ANativeWindow::query = hook_query; } static bool isValid(const sp<Surface>& surface) { return surface != nullptr; } void allocateBuffers() {} @@ -48,7 +51,11 @@ public: return 0; } virtual int unlockAndPost() { return 0; } - virtual int query(int what, int* value) const { return 0; } + virtual int query(int what, int* value) const { return mBufferProducer->query(what, value); } + + status_t setDequeueTimeout(nsecs_t timeout) { return OK; } + + nsecs_t getLastDequeueStartTime() const { return 0; } virtual void destroy() {} @@ -57,12 +64,44 @@ public: protected: virtual ~Surface() {} - static int hook_perform(ANativeWindow* window, int operation, ...) { return 0; } + static int hook_perform(ANativeWindow* window, int operation, ...) { + va_list args; + va_start(args, operation); + Surface* c = getSelf(window); + int result = c->perform(operation, args); + va_end(args); + return result; + } + + static int hook_query(const ANativeWindow* window, int what, int* value) { + const Surface* c = getSelf(window); + return c->query(what, value); + } + + static int hook_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, + int* fenceFd) { + Surface* c = getSelf(window); + return c->dequeueBuffer(buffer, fenceFd); + } + + virtual int dequeueBuffer(ANativeWindowBuffer** buffer, int* fenceFd) { + mBufferProducer->requestBuffer(0, &mBuffer); + *buffer = mBuffer.get(); + return OK; + } + virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd) { return 0; } + virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd) { return 0; } + virtual int perform(int operation, va_list args) { return 0; } + virtual int setSwapInterval(int interval) { return 0; } + virtual int setBufferCount(int bufferCount) { return 0; } private: // can't be copied Surface& operator=(const Surface& rhs); Surface(const Surface& rhs); + + const sp<IGraphicBufferProducer> mBufferProducer; + sp<GraphicBuffer> mBuffer; }; } // namespace android diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/ui/GraphicBuffer.h index ac88e44dbc65..eec9b235cd6a 100644 --- a/libs/hostgraphics/ui/GraphicBuffer.h +++ b/libs/hostgraphics/ui/GraphicBuffer.h @@ -22,24 +22,27 @@ #include <vector> +#include <ui/ANativeObjectBase.h> #include <ui/PixelFormat.h> #include <ui/Rect.h> - #include <utils/RefBase.h> namespace android { -class GraphicBuffer : virtual public RefBase { +class GraphicBuffer : public ANativeObjectBase<ANativeWindowBuffer, GraphicBuffer, RefBase> { public: - GraphicBuffer(uint32_t w, uint32_t h):width(w),height(h) { + GraphicBuffer(uint32_t w, uint32_t h) { data.resize(w*h); + reserved[0] = data.data(); + width = w; + height = h; } uint32_t getWidth() const { return static_cast<uint32_t>(width); } uint32_t getHeight() const { return static_cast<uint32_t>(height); } uint32_t getStride() const { return static_cast<uint32_t>(width); } uint64_t getUsage() const { return 0; } PixelFormat getPixelFormat() const { return PIXEL_FORMAT_RGBA_8888; } - //uint32_t getLayerCount() const { return static_cast<uint32_t>(layerCount); } + Rect getBounds() const { return Rect(width, height); } status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, @@ -54,8 +57,6 @@ public: status_t unlockAsync(int *fenceFd) { return OK; } private: - uint32_t width; - uint32_t height; std::vector<uint32_t> data; }; diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index b87002371775..8e07a2f31de1 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -15,14 +15,18 @@ */ #include "ShaderCache.h" + #include <GrDirectContext.h> #include <SkData.h> #include <gui/TraceUtils.h> #include <log/log.h> #include <openssl/sha.h> + #include <algorithm> #include <array> +#include <mutex> #include <thread> + #include "FileBlobCache.h" #include "Properties.h" diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp index 6993d5240187..7a155c583fd4 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.cpp +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -45,7 +45,7 @@ void HintSessionWrapper::HintSessionBinding::init() { LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!"); BIND_APH_METHOD(getManager); - BIND_APH_METHOD(createSession); + BIND_APH_METHOD(createSessionInternal); BIND_APH_METHOD(closeSession); BIND_APH_METHOD(updateTargetWorkDuration); BIND_APH_METHOD(reportActualWorkDuration); @@ -122,7 +122,8 @@ bool HintSessionWrapper::init() { int64_t targetDurationNanos = mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration; mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] { - return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos); + return mBinding->createSessionInternal(manager, tids.data(), tids.size(), + targetDurationNanos, SessionTag::HWUI); }); return false; } diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 14e7a53fd94f..859cc57dea9f 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -17,6 +17,7 @@ #pragma once #include <android/performance_hint.h> +#include <private/performance_hint_private.h> #include <future> #include <optional> @@ -80,9 +81,10 @@ private: virtual ~HintSessionBinding() = default; virtual void init(); APerformanceHintManager* (*getManager)(); - APerformanceHintSession* (*createSession)(APerformanceHintManager* manager, - const int32_t* tids, size_t tidCount, - int64_t defaultTarget) = nullptr; + APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager, + const int32_t* tids, size_t tidCount, + int64_t defaultTarget, + SessionTag tag) = nullptr; void (*closeSession)(APerformanceHintSession* session) = nullptr; void (*updateTargetWorkDuration)(APerformanceHintSession* session, int64_t targetDuration) = nullptr; diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp index c16602c29e2a..a8db0f4aa4f0 100644 --- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp +++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp @@ -52,8 +52,8 @@ protected: void init() override; MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ()); - MOCK_METHOD(APerformanceHintSession*, fakeCreateSession, - (APerformanceHintManager*, const int32_t*, size_t, int64_t)); + MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal, + (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag)); MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*)); MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t)); MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t)); @@ -72,22 +72,28 @@ protected: // Must be static so we can point to them as normal fn pointers with HintSessionBinding static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); }; - static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, size_t idsSize, + int64_t initialTarget, + SessionTag tag) { + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubManagedCreateSessionInternal( + APerformanceHintManager* manager, const int32_t* ids, size_t idsSize, + int64_t initialTarget, SessionTag tag) { sMockBinding->allowCreationToFinish.get_future().wait(); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, + size_t idsSize, + int64_t initialTarget, + SessionTag tag) { std::this_thread::sleep_for(50ms); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } static void stubCloseSession(APerformanceHintSession* session) { sMockBinding->fakeCloseSession(session); @@ -139,14 +145,14 @@ void HintSessionWrapperTests::SetUp() { mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId); mWrapper->mBinding = sMockBinding; EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr)); - ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr)); + ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr)); ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0)); } void HintSessionWrapperTests::MockHintSessionBinding::init() { sMockBinding->getManager = &stubGetManager; - if (sMockBinding->createSession == nullptr) { - sMockBinding->createSession = &stubCreateSession; + if (sMockBinding->createSessionInternal == nullptr) { + sMockBinding->createSessionInternal = &stubCreateSessionInternal; } sMockBinding->closeSession = &stubCloseSession; sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration; @@ -163,14 +169,14 @@ void HintSessionWrapperTests::TearDown() { TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) { EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = stubSlowCreateSession; + sMockBinding->createSessionInternal = stubSlowCreateSessionInternal; mWrapper->init(); mWrapper = nullptr; Mock::VerifyAndClearExpectations(sMockBinding.get()); } TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) { - EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1); mWrapper->init(); waitForWrapperReady(); } @@ -219,7 +225,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesBeforeAsyncCreationFinish // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens after EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -246,7 +252,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens before EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -352,7 +358,7 @@ TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct } TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) { - EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1); EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22}))) .Times(1); mWrapper->init(); |