From fe8e00bfd5cae8023b25d82b9996476ebfc43708 Mon Sep 17 00:00:00 2001 From: Paul Sowden Date: Fri, 15 Nov 2024 09:10:23 -0800 Subject: [PATCH 1/9] Reset static state in ShadowLegacyChoreographer between tests Reset the 'postCallbackDelayMillis' and 'postFrameCallbackDelayMillis' fields. PiperOrigin-RevId: 696907439 --- .../java/org/robolectric/shadows/ShadowLegacyChoreographer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyChoreographer.java index 5dd5e60dd09..8d01deb103d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyChoreographer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyChoreographer.java @@ -182,5 +182,7 @@ public static synchronized void reset() { } instance = makeThreadLocal(); FRAME_INTERVAL = Duration.ofMillis(10).toNanos(); + postCallbackDelayMillis = 0; + postFrameCallbackDelayMillis = 0; } } From 78dbb80dc835b12415e36f2a57e37dac63122122 Mon Sep 17 00:00:00 2001 From: Hunter Knepshield Date: Fri, 15 Nov 2024 15:37:00 -0800 Subject: [PATCH 2/9] Add shadow implementation of `SubscriptionManager`'s APIs to convert from a slot ID -> subscription ID. This API has evolved a few times over the years and allows apps permissions-free access to a subscription ID handle, even if they do not have access to the underlying `SubscriptionInfo`. Since some of these converters are `static` (including the most-current `SubscriptionManager.getSubscriptionId` API added in U), we also need to make the "active" `SubscriptionInfo` list `static`. PiperOrigin-RevId: 697020322 --- .../ShadowSubscriptionManagerTest.java | 75 +++++++++++++++++++ .../shadows/ShadowSubscriptionManager.java | 51 ++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java index 1866a23b471..92dc8fb14e8 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java @@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -404,6 +405,80 @@ public void getPhoneId_shouldReturnInvalidIfReset() { .isEqualTo(ShadowSubscriptionManager.INVALID_PHONE_INDEX); } + @Test + public void getSubId() { + // Explicitly callable without any permissions. + shadowOf(subscriptionManager).setReadPhoneStatePermission(false); + + assertThat(SubscriptionManager.getSubId(/* slotIndex= */ 0)).isNull(); + + shadowOf(subscriptionManager) + .setActiveSubscriptionInfos( + SubscriptionInfoBuilder.newBuilder() + .setId(123) + .setSimSlotIndex(0) + .buildSubscriptionInfo(), + SubscriptionInfoBuilder.newBuilder() + .setId(456) + .setSimSlotIndex(1) + .buildSubscriptionInfo()); + int[] subId = SubscriptionManager.getSubId(/* slotIndex= */ 0); + assertThat(subId).hasLength(1); + assertThat(subId[0]).isEqualTo(123); + + assertThat(SubscriptionManager.getSubId(/* slotIndex= */ 2)).isNull(); + } + + @Test + @Config(minSdk = Q) + public void getSubscriptionIds() { + // Explicitly callable without any permissions. + shadowOf(subscriptionManager).setReadPhoneStatePermission(false); + + assertThat(subscriptionManager.getSubscriptionIds(/* slotIndex= */ 0)).isNull(); + + shadowOf(subscriptionManager) + .setActiveSubscriptionInfos( + SubscriptionInfoBuilder.newBuilder() + .setId(123) + .setSimSlotIndex(0) + .buildSubscriptionInfo(), + SubscriptionInfoBuilder.newBuilder() + .setId(456) + .setSimSlotIndex(1) + .buildSubscriptionInfo()); + int[] subId = subscriptionManager.getSubscriptionIds(/* slotIndex= */ 0); + assertThat(subId).hasLength(1); + assertThat(subId[0]).isEqualTo(123); + + assertThat(subscriptionManager.getSubscriptionIds(/* slotIndex= */ 2)).isNull(); + } + + @Test + @Config(minSdk = UPSIDE_DOWN_CAKE) + public void getSubscriptionId() { + // Explicitly callable without any permissions. + shadowOf(subscriptionManager).setReadPhoneStatePermission(false); + + assertThat(SubscriptionManager.getSubscriptionId(/* slotIndex= */ 0)) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID); + + shadowOf(subscriptionManager) + .setActiveSubscriptionInfos( + SubscriptionInfoBuilder.newBuilder() + .setId(123) + .setSimSlotIndex(0) + .buildSubscriptionInfo(), + SubscriptionInfoBuilder.newBuilder() + .setId(456) + .setSimSlotIndex(1) + .buildSubscriptionInfo()); + assertThat(SubscriptionManager.getSubscriptionId(/* slotIndex= */ 0)).isEqualTo(123); + + assertThat(SubscriptionManager.getSubscriptionId(/* slotIndex= */ 2)) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + @Test public void setMcc() { assertThat( diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java index b40bbd29a48..d9adb94cd97 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java @@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import android.os.Build.VERSION; import android.telephony.SubscriptionInfo; @@ -128,19 +129,19 @@ public static void setDefaultVoiceSubscriptionId(int defaultVoiceSubscriptionId) /** * Cache of {@link SubscriptionInfo} used by {@link #getActiveSubscriptionInfoList}. Managed by - * {@link #setActiveSubscriptionInfoList}. + * {@link #setActiveSubscriptionInfoList}. May be {@code null}. */ - private List subscriptionList = new ArrayList<>(); + private static List subscriptionList = new ArrayList<>(); /** * Cache of {@link SubscriptionInfo} used by {@link #getAccessibleSubscriptionInfoList}. Managed - * by {@link #setAccessibleSubscriptionInfos}. + * by {@link #setAccessibleSubscriptionInfos}. May be {@code null}. */ private List accessibleSubscriptionList = new ArrayList<>(); /** * Cache of {@link SubscriptionInfo} used by {@link #getAvailableSubscriptionInfoList}. Managed by - * {@link #setAvailableSubscriptionInfos}. + * {@link #setAvailableSubscriptionInfos}. May be {@code null}. */ private List availableSubscriptionList = new ArrayList<>(); @@ -466,6 +467,47 @@ protected static int getPhoneId(int subId) { return INVALID_PHONE_INDEX; } + /** + * Older form of {@link #getSubscriptionId} that was designed prior to mainstream multi-SIM + * support, so its {@code int[]} return type ended up being an unused vestige from that older + * design. + */ + @Implementation(minSdk = LOLLIPOP_MR1) + @HiddenApi + protected static int[] getSubId(int slotIndex) { + int subId = getSubscriptionId(slotIndex); + return subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID ? null : new int[] {subId}; + } + + /** + * Older form of {@link #getSubscriptionId} that was designed prior to mainstream multi-SIM + * support, so its {@code int[]} return type ended up being an unused vestige from that older + * design. + */ + @Implementation(minSdk = Q) + protected int[] getSubscriptionIds(int slotIndex) { + return getSubId(slotIndex); + } + + /** + * Derives the subscription ID corresponding to an "active" {@link SubscriptionInfo} for the given + * SIM slot index. + */ + @Implementation(minSdk = UPSIDE_DOWN_CAKE) + protected static int getSubscriptionId(int slotIndex) { + // Intentionally not re-calling getActiveSubscriptionInfoForSimSlotIndex since this API does not + // require any permissions (and this is static). + if (subscriptionList == null) { + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + for (SubscriptionInfo info : subscriptionList) { + if (info.getSimSlotIndex() == slotIndex) { + return info.getSubscriptionId(); + } + } + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + /** * When set to false methods requiring {@link android.Manifest.permission.READ_PHONE_STATE} * permission will throw a {@link SecurityException}. By default it's set to true for backwards @@ -533,6 +575,7 @@ public static void reset() { defaultSmsSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; defaultVoiceSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; defaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + subscriptionList = new ArrayList<>(); phoneIds.clear(); phoneNumberMap.clear(); readPhoneStatePermission = true; From 361375be36f25ed2d2bdbce6a79b50ddbf79fb30 Mon Sep 17 00:00:00 2001 From: Paul Sowden Date: Mon, 18 Nov 2024 11:20:53 -0800 Subject: [PATCH 3/9] Add realistic support for window insets Adds support for window insets by implementing behavior of the IWindowSession which the ViewRootImpl interacts with when adding and modifying its window. The implementations responds to ViewRootImpl calls and interacts with the IWindow handle to coordinate the visibility of insets as well as the bounds of the window. This allows more realistic behavior for more than just window insets, including focus and window positioning (note that fixed support for window sizing is further flagged to allow a gradual migration). Introduces a new SystemUi API for allowing configuring and controlling the window insets behavior per display. This API is currently not publicly exposed but it is anticipated once we are happy with the API and behavior this, or another, API will be made public allowing tests to configure window insets behavior. As window insets have changed in many Android releases (which contributes to the complexity of implementing support in Robolectric) its particularly useful to test faithful behavior across SDKs. PiperOrigin-RevId: 697697010 --- .../shadows/ShadowInsetsControllerTest.java | 72 --- .../shadows/ShadowViewRootImplTest.java | 55 -- .../ShadowWindowManagerGlobalTest.java | 160 +++++- .../controller/ActivityController.java | 1 - .../shadows/ShadowDisplayManagerGlobal.java | 11 + .../shadows/ShadowInsetsAnimationThread.java | 16 + .../shadows/ShadowInsetsController.java | 77 --- .../shadows/ShadowInsetsSource.java | 57 ++ .../shadows/ShadowInsetsState.java | 83 +++ .../shadows/ShadowNativeHardwareRenderer.java | 9 +- .../robolectric/shadows/ShadowSurface.java | 5 + .../shadows/ShadowUiAutomation.java | 22 +- .../org/robolectric/shadows/ShadowView.java | 36 -- .../shadows/ShadowViewRootImpl.java | 395 +------------ .../shadows/ShadowWindowManagerGlobal.java | 480 +++++++++++++++- .../shadows/ShadowWindowManagerImpl.java | 7 +- .../org/robolectric/shadows/SystemUi.java | 534 ++++++++++++++++++ 17 files changed, 1378 insertions(+), 642 deletions(-) delete mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java delete mode 100644 robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsAnimationThread.java delete mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsSource.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsState.java create mode 100644 shadows/framework/src/main/java/org/robolectric/shadows/SystemUi.java diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java deleted file mode 100644 index 45c428416cf..00000000000 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.robolectric.shadows; - -import static com.google.common.truth.Truth.assertThat; - -import android.app.Activity; -import android.os.Build; -import android.view.WindowInsets; -import android.view.WindowInsetsController; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; - -@RunWith(AndroidJUnit4.class) -@Config(minSdk = Build.VERSION_CODES.R) -public class ShadowInsetsControllerTest { - private ActivityController activityController; - private Activity activity; - private WindowInsetsController controller; - - @Before - public void setUp() { - activityController = Robolectric.buildActivity(Activity.class); - activityController.setup(); - - activity = activityController.get(); - controller = activity.getWindow().getInsetsController(); - } - - @Test - public void statusBar_show_hide_trackedByWindowInsets() { - // Responds to hide. - controller.hide(WindowInsets.Type.statusBars()); - assertStatusBarVisibility(/* isVisible= */ false); - - // Responds to show. - controller.show(WindowInsets.Type.statusBars()); - assertStatusBarVisibility(/* isVisible= */ true); - - // Does not respond to different type. - controller.hide(WindowInsets.Type.navigationBars()); - assertStatusBarVisibility(/* isVisible= */ true); - } - - @Test - public void navigationBar_show_hide_trackedByWindowInsets() { - // Responds to hide. - controller.hide(WindowInsets.Type.navigationBars()); - assertNavigationBarVisibility(/* isVisible= */ false); - - // Responds to show. - controller.show(WindowInsets.Type.navigationBars()); - assertNavigationBarVisibility(/* isVisible= */ true); - - // Does not respond to different type. - controller.hide(WindowInsets.Type.statusBars()); - assertNavigationBarVisibility(/* isVisible= */ true); - } - - private void assertStatusBarVisibility(boolean isVisible) { - WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets(); - assertThat(insets.isVisible(WindowInsets.Type.statusBars())).isEqualTo(isVisible); - } - - private void assertNavigationBarVisibility(boolean isVisible) { - WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets(); - assertThat(insets.isVisible(WindowInsets.Type.navigationBars())).isEqualTo(isVisible); - } -} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java deleted file mode 100644 index e10cbb3fa71..00000000000 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.robolectric.shadows; - -import static com.google.common.truth.Truth.assertThat; - -import android.app.Activity; -import android.os.Build; -import android.view.View; -import android.view.WindowInsets; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; - -@RunWith(AndroidJUnit4.class) -public class ShadowViewRootImplTest { - private ActivityController activityController; - private Activity activity; - private View rootView; - - @Before - public void setUp() { - activityController = Robolectric.buildActivity(Activity.class); - activityController.setup(); - - activity = activityController.get(); - rootView = activity.getWindow().getDecorView(); - } - - @Test - @Config(minSdk = Build.VERSION_CODES.R) - public void setIsStatusBarVisible_impactsGetWindowInsets() { - ShadowViewRootImpl.setIsStatusBarVisible(false); - WindowInsets windowInsets = rootView.getRootWindowInsets(); - assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isFalse(); - - ShadowViewRootImpl.setIsStatusBarVisible(true); - windowInsets = rootView.getRootWindowInsets(); - assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isTrue(); - } - - @Test - @Config(minSdk = Build.VERSION_CODES.R) - public void setIsNavigationBarVisible_impactsGetWindowInsets() { - ShadowViewRootImpl.setIsNavigationBarVisible(false); - WindowInsets windowInsets = rootView.getRootWindowInsets(); - assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isFalse(); - - ShadowViewRootImpl.setIsNavigationBarVisible(true); - windowInsets = rootView.getRootWindowInsets(); - assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isTrue(); - } -} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java index 94adedcea08..1409453fe3a 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWindowManagerGlobalTest.java @@ -1,10 +1,21 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; +import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.robolectric.Robolectric.buildActivity; +import static org.robolectric.shadows.ShadowLooper.idleMainLooper; +import static org.robolectric.shadows.SystemUi.STANDARD_STATUS_BAR; +import static org.robolectric.shadows.SystemUi.THREE_BUTTON_NAVIGATION; +import static org.robolectric.shadows.SystemUi.systemUiForDefaultDisplay; import android.app.Activity; import android.content.ClipData; +import android.graphics.Color; import android.graphics.Rect; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -14,6 +25,9 @@ import android.view.View.DragShadowBuilder; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; import android.window.BackEvent; import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedDispatcher; @@ -34,10 +48,11 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.GraphicsMode; import org.robolectric.annotation.GraphicsMode.Mode; +import org.robolectric.shadows.SystemUi.NavigationBar; +import org.robolectric.shadows.SystemUi.StatusBar; @RunWith(AndroidJUnit4.class) @GraphicsMode(Mode.LEGACY) - public class ShadowWindowManagerGlobalTest { @Before @@ -272,4 +287,147 @@ public void onBackCancelled() { onBackCancelledCalled = true; } } + + @Test + public void windowInsets() { + systemUiForDefaultDisplay().setBehavior(STANDARD_STATUS_BAR, THREE_BUTTON_NAVIGATION); + ActivityController controller = buildActivity(WindowInsetsActivity.class); + + controller.setup(); + idleMainLooper(); + + StatusBar statusBar = systemUiForDefaultDisplay().getStatusBar(); + NavigationBar navBar = systemUiForDefaultDisplay().getNavigationBar(); + assertThat(controller.get().systemInsets) + .isEqualTo(new Rect(0, statusBar.getSize(), 0, navBar.getSize())); + assertThat(controller.get().windowInsets).isNotNull(); + } + + @Config(minSdk = R) + @Test + public void windowInsetsController_hideStatusBar() { + systemUiForDefaultDisplay().setBehavior(STANDARD_STATUS_BAR, THREE_BUTTON_NAVIGATION); + ActivityController controller = buildActivity(WindowInsetsActivity.class); + controller.setup(); + idleMainLooper(); + + controller.get().getWindow().getInsetsController().hide(WindowInsets.Type.statusBars()); + idleMainLooper(); + + assertThat(controller.get().windowInsets.getInsets(WindowInsets.Type.statusBars()).top) + .isEqualTo(0); + assertThat(controller.get().windowInsets.isVisible(WindowInsets.Type.statusBars())).isFalse(); + assertThat(controller.get().windowInsets.isVisible(WindowInsets.Type.navigationBars())) + .isTrue(); + } + + @Config(minSdk = R) + @Test + public void windowInsetsController_hideSystemBars() { + systemUiForDefaultDisplay().setBehavior(STANDARD_STATUS_BAR, THREE_BUTTON_NAVIGATION); + ActivityController controller = buildActivity(WindowInsetsActivity.class); + controller.setup(); + idleMainLooper(); + + controller.get().getWindow().getInsetsController().hide(WindowInsets.Type.systemBars()); + idleMainLooper(); + + assertThat(controller.get().windowInsets.isVisible(WindowInsets.Type.statusBars())).isFalse(); + assertThat(controller.get().windowInsets.isVisible(WindowInsets.Type.navigationBars())) + .isFalse(); + } + + @Config(minSdk = R) + @Test + public void windowInsetsController_toggleStatusBar() { + ActivityController controller = buildActivity(WindowInsetsActivity.class); + controller.setup(); + idleMainLooper(); + + controller.get().getWindow().getInsetsController().hide(WindowInsets.Type.statusBars()); + idleMainLooper(); + controller.get().getWindow().getInsetsController().show(WindowInsets.Type.statusBars()); + idleMainLooper(); + + assertThat(controller.get().windowInsets.isVisible(WindowInsets.Type.statusBars())) + .isEqualTo(true); + } + + @Config(minSdk = R) + @Test + public void windowInsetsController_twoWindows_toggleStatusBar() { + ActivityController controller = buildActivity(WindowInsetsActivity.class); + controller.setup(); + idleMainLooper(); + ActivityController controller2 = + buildActivity(WindowInsetsActivity.class); + controller2.setup(); + idleMainLooper(); + + controller2.get().getWindow().getInsetsController().hide(WindowInsets.Type.statusBars()); + idleMainLooper(); + + assertThat(controller2.get().windowInsets.isVisible(WindowInsets.Type.statusBars())) + .isEqualTo(false); + } + + public static final class WindowInsetsActivity extends Activity { + Rect systemInsets; + WindowInsets windowInsets; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + setEdgeToEdge(getWindow()); + super.onCreate(savedInstanceState); + setContentView( + new View(this) { + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + windowInsets = insets; + return super.onApplyWindowInsets(insets); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + systemInsets = new Rect(insets); + return super.fitSystemWindows(insets); + } + }); + } + } + + // This sets similar properties to the androidx edgeToEdge API. + private static void setEdgeToEdge(Window window) { + if (RuntimeEnvironment.getApiLevel() <= Q) { + window + .getDecorView() + .setSystemUiVisibility( + window.getDecorView().getSystemUiVisibility() + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } else if (RuntimeEnvironment.getApiLevel() <= UPSIDE_DOWN_CAKE) { + window + .getDecorView() + .setSystemUiVisibility( + window.getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + window.setDecorFitsSystemWindows(false); + } else { + window.setDecorFitsSystemWindows(false); + } + if (RuntimeEnvironment.getApiLevel() <= LOLLIPOP_MR1) { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } else { + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); + } + if (RuntimeEnvironment.getApiLevel() > R) { + window.getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + } else if (RuntimeEnvironment.getApiLevel() > P) { + window.getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + } } diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java index ab4f09b998f..a736e6fcd22 100644 --- a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java +++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java @@ -234,7 +234,6 @@ public ActivityController visible() { // root can be null if activity does not have content attached, or if looper is paused. // this is unusual but leave the check here for legacy compatibility if (root != null) { - callDispatchResized(root); shadowMainLooper.idleIfPaused(); } return this; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java index c19442a850a..d5656615f39 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java @@ -148,6 +148,10 @@ void removeDisplay(int displayId) { mDm.removeDisplay(displayId); } + SystemUi getSystemUi(int displayId) { + return mDm.getSystemUi(displayId); + } + /** * A delegating proxy for the IDisplayManager system service. * @@ -157,6 +161,7 @@ void removeDisplay(int displayId) { */ private static class DisplayManagerProxyDelegate { private final TreeMap displayInfos = new TreeMap<>(); + private final Map systemUis = new HashMap<>(); private int nextDisplayId = 0; private final List callbacks = new ArrayList<>(); private final Map virtualDisplayIds = new HashMap<>(); @@ -183,6 +188,10 @@ public int[] getDisplayIds(boolean ignoredIncludeDisabled) { return getDisplayIds(); } + public SystemUi getSystemUi(int displayId) { + return systemUis.get(displayId); + } + // @Override public void registerCallback(IDisplayManagerCallback iDisplayManagerCallback) throws RemoteException { @@ -299,6 +308,7 @@ private synchronized int addDisplay(DisplayInfo displayInfo) { if (RuntimeEnvironment.getApiLevel() >= Q) { displayInfo.displayId = nextId; } + systemUis.put(nextId, new SystemUi(nextId)); notifyListeners(nextId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED); return nextId; } @@ -318,6 +328,7 @@ private synchronized void removeDisplay(int displayId) { } displayInfos.remove(displayId); + systemUis.remove(displayId); notifyListeners(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsAnimationThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsAnimationThread.java new file mode 100644 index 00000000000..231f7ab87e9 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsAnimationThread.java @@ -0,0 +1,16 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.R; + +import android.view.InsetsAnimationThread; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +/** Shadow for {@link InsetsAnimationThread}. */ +@Implements(value = InsetsAnimationThread.class, minSdk = R, isInAndroidSdk = false) +public class ShadowInsetsAnimationThread { + @Resetter + public static void reset() { + InsetsAnimationThread.release(); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java deleted file mode 100644 index 0cd93e68e2f..00000000000 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.robolectric.shadows; - -import android.annotation.RequiresApi; -import android.os.Build; -import android.view.InsetsController; -import android.view.WindowInsets; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; -import org.robolectric.annotation.ReflectorObject; -import org.robolectric.util.reflector.Direct; -import org.robolectric.util.reflector.ForType; - -/** Intercepts calls to [InsetsController] to monitor system bars functionality (hide/show). */ -@Implements(value = InsetsController.class, minSdk = Build.VERSION_CODES.R, isInAndroidSdk = false) -@RequiresApi(Build.VERSION_CODES.R) -public class ShadowInsetsController { - @ReflectorObject private InsetsControllerReflector insetsControllerReflector; - - /** - * Intercepts calls to [InsetsController.show] to detect requested changes to the system - * status/nav bar visibility. - */ - @Implementation - protected void show(int types) { - if (hasStatusBarType(types)) { - ShadowViewRootImpl.setIsStatusBarVisible(true); - } - - if (hasNavigationBarType(types)) { - ShadowViewRootImpl.setIsNavigationBarVisible(true); - } - - insetsControllerReflector.show(types); - } - - /** - * Intercepts calls to [InsetsController.hide] to detect requested changes to the system - * status/nav bar visibility. - */ - @Implementation - public void hide(int types) { - if (hasStatusBarType(types)) { - ShadowViewRootImpl.setIsStatusBarVisible(false); - } - - if (hasNavigationBarType(types)) { - ShadowViewRootImpl.setIsNavigationBarVisible(false); - } - - insetsControllerReflector.hide(types); - } - - /** Returns true if the given flags contain the mask for the system status bar. */ - private boolean hasStatusBarType(int types) { - return hasTypeMask(types, WindowInsets.Type.statusBars()); - } - - /** Returns true if the given flags contain the mask for the system navigation bar. */ - private boolean hasNavigationBarType(int types) { - return hasTypeMask(types, WindowInsets.Type.navigationBars()); - } - - /** Returns true if the given flags contains the requested type mask. */ - private boolean hasTypeMask(int types, int typeMask) { - return (types & typeMask) == typeMask; - } - - /** Reflector for [InsetsController] to use for direct (non-intercepted) calls. */ - @ForType(InsetsController.class) - interface InsetsControllerReflector { - @Direct - void show(int types); - - @Direct - void hide(int types); - } -} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsSource.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsSource.java new file mode 100644 index 00000000000..573e6394c73 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsSource.java @@ -0,0 +1,57 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + +import android.graphics.Rect; +import android.view.InsetsSource; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.ReflectorObject; +import org.robolectric.util.reflector.ForType; + +/** Shadow of {@link InsetsSource}. */ +@Implements(value = InsetsSource.class, minSdk = Q, isInAndroidSdk = false) +public class ShadowInsetsSource { + @RealObject private InsetsSource realInsetsSource; + @ReflectorObject private InsetsSourceReflector insetsSourceReflector; + + /** + * Backwards compatible version of {@link InsetsSource#setVisible(boolean)} which changed in U + * from returning {@code void} to {@link InsetsSource}. + */ + @CanIgnoreReturnValue + ShadowInsetsSource setVisible(boolean isVisible) { + if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) { + realInsetsSource.setVisible(isVisible); + } else { + insetsSourceReflector.setVisible(isVisible); + } + return this; + } + + /** + * Backwards compatible version of {@link InsetsSource#setFrame(Rect)} which changed in U from + * returning {@code void} to {@link InsetsSource}. + */ + @CanIgnoreReturnValue + ShadowInsetsSource setFrame(Rect frame) { + if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) { + realInsetsSource.setFrame(frame); + } else { + insetsSourceReflector.setFrame(frame); + } + return this; + } + + @ForType(InsetsSource.class) + interface InsetsSourceReflector { + // Prior to U this method returned void + void setFrame(Rect frame); + + // Prior to U this method returned void + void setVisible(boolean isVisible); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsState.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsState.java new file mode 100644 index 00000000000..bd343e271fa --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsState.java @@ -0,0 +1,83 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static org.robolectric.util.reflector.Reflector.reflector; + +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.WindowInsets; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.ReflectorObject; +import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.ForType; +import org.robolectric.util.reflector.Static; + +/** Shadow of {@link InsetsState}. */ +@Implements(value = InsetsState.class, minSdk = Q, isInAndroidSdk = false) +public class ShadowInsetsState { + // These must align with the indexes declared in InsetsState in SDK up to 33 + static final int STATUS_BARS = 0; + static final int NAVIGATION_BARS = 1; + + @RealObject private InsetsState realInsetsState; + @ReflectorObject private InsetsStateReflector insetsStateReflector; + + InsetsSource getOrCreateSource(int id) { + return RuntimeEnvironment.getApiLevel() < UPSIDE_DOWN_CAKE + ? insetsStateReflector.getSource(id) + : realInsetsState.getOrCreateSource(id, getType(id)); + } + + int getSourceSize() { + if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) { + return realInsetsState.sourceSize(); + } else if (RuntimeEnvironment.getApiLevel() >= R) { + return reflector(InsetsStateReflector.class).getLastType() + 1; + } else { + return insetsStateReflector.getSourcesCount(); + } + } + + private static int getType(int id) { + switch (id) { + case STATUS_BARS: + return RuntimeEnvironment.getApiLevel() < Q + ? reflector(WindowInsetsTypeReflector.class).topBar() + : WindowInsets.Type.statusBars(); + case NAVIGATION_BARS: + return RuntimeEnvironment.getApiLevel() < Q + ? reflector(WindowInsetsTypeReflector.class).sideBars() + : WindowInsets.Type.navigationBars(); + default: + throw new IllegalArgumentException(); + } + } + + @ForType(InsetsState.class) + interface InsetsStateReflector { + InsetsSource getSource(int type); + + @Accessor("ITYPE_IME") + @Static + int getImeType(); + + @Accessor("LAST_TYPE") + @Static + int getLastType(); + + int getSourcesCount(); + } + + @ForType(WindowInsets.Type.class) + interface WindowInsetsTypeReflector { + @Static + int topBar(); + + @Static + int sideBars(); + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java index 732fa9a732b..2ca5fbf7ad1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java @@ -54,6 +54,11 @@ protected static boolean isWebViewOverlaysEnabled() { return HardwareRendererNatives.isWebViewOverlaysEnabled(); } + @Implementation(minSdk = TIRAMISU, maxSdk = U.SDK_INT) + protected static boolean nIsDrawingEnabled() { + return true; + } + @Implementation(maxSdk = U.SDK_INT) protected static void setupShadersDiskCache(String cacheFile, String skiaCacheFile) { HardwareRendererNatives.setupShadersDiskCache(cacheFile, skiaCacheFile); @@ -121,9 +126,9 @@ protected static void nSetSurface(long nativeProxy, Surface window, boolean disc HardwareRendererNatives.nSetSurface(nativeProxy, window, discardBuffer); } - @Implementation(minSdk = S, maxSdk = U.SDK_INT) + @Implementation(minSdk = S) protected static void nSetSurfaceControl(long nativeProxy, long nativeSurfaceControl) { - HardwareRendererNatives.nSetSurfaceControl(nativeProxy, nativeSurfaceControl); + // SurfaceControl is not in RNG } @Implementation(maxSdk = U.SDK_INT) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurface.java index 30201e0ecec..6c70c41ea21 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurface.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurface.java @@ -58,6 +58,11 @@ protected void finalize() throws Throwable { surfaceReflector.finalize(); } + @Implementation + protected void checkNotReleasedLocked() { + checkNotReleased(); + } + @Implementation protected boolean isValid() { return valid.get(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java index 932b052f088..61f70c98b32 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUiAutomation.java @@ -137,7 +137,8 @@ protected Bitmap takeScreenshot() throws Exception { Canvas windowCanvas = new Canvas(window); rootView.draw(windowCanvas); } - screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint); + screenshotCanvas.drawBitmap( + window, root.locationOnScreen.x, root.locationOnScreen.y, paint); } return screenshot; }); @@ -181,14 +182,14 @@ private static boolean injectMotionEvent(MotionEvent event) { for (int i = 0; i < touchableRoots.size(); i++) { Root root = touchableRoots.get(i); if (i == touchableRoots.size() - 1 || root.isTouchModal() || root.isTouchInside(event)) { - event.offsetLocation(-root.params.x, -root.params.y); + event.offsetLocation(-root.locationOnScreen.x, -root.locationOnScreen.y); root.getRootView().dispatchTouchEvent(event); - event.offsetLocation(root.params.x, root.params.y); + event.offsetLocation(root.locationOnScreen.x, root.locationOnScreen.y); break; } else if (event.getActionMasked() == MotionEvent.ACTION_DOWN && root.watchTouchOutside()) { MotionEvent outsideEvent = MotionEvent.obtain(event); outsideEvent.setAction(MotionEvent.ACTION_OUTSIDE); - outsideEvent.offsetLocation(-root.params.x, -root.params.y); + outsideEvent.offsetLocation(-root.locationOnScreen.x, -root.locationOnScreen.y); root.getRootView().dispatchTouchEvent(outsideEvent); outsideEvent.recycle(); } @@ -283,11 +284,16 @@ private static final class Root { final ViewRootImpl impl; final WindowManager.LayoutParams params; final int index; + final Point locationOnScreen; Root(ViewRootImpl impl, WindowManager.LayoutParams params, int index) { this.impl = impl; this.params = params; this.index = index; + + int[] coords = new int[2]; + getRootView().getLocationOnScreen(coords); + locationOnScreen = new Point(coords[0], coords[1]); } int getIndex() { @@ -304,10 +310,10 @@ View getRootView() { boolean isTouchInside(MotionEvent event) { int index = event.getActionIndex(); - return event.getX(index) >= params.x - && event.getX(index) <= params.x + impl.getView().getWidth() - && event.getY(index) >= params.y - && event.getY(index) <= params.y + impl.getView().getHeight(); + return event.getX(index) >= locationOnScreen.x + && event.getX(index) <= locationOnScreen.x + impl.getView().getWidth() + && event.getY(index) >= locationOnScreen.y + && event.getY(index) <= locationOnScreen.y + impl.getView().getHeight(); } boolean isTouchModal() { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java index 1887043237a..daaec197791 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java @@ -14,7 +14,6 @@ import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Looper; @@ -27,10 +26,8 @@ import android.view.IWindowId; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.view.WindowId; -import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.Transformation; import com.google.common.annotations.Beta; @@ -631,33 +628,6 @@ protected void setScrollY(int scrollY) { } } - @Implementation - protected void getLocationOnScreen(int[] outLocation) { - reflector(_View_.class, realView).getLocationOnScreen(outLocation); - int[] windowLocation = getWindowLocation(); - outLocation[0] += windowLocation[0]; - outLocation[1] += windowLocation[1]; - } - - @Implementation(minSdk = O) - protected void mapRectFromViewToScreenCoords(RectF rect, boolean clipToParent) { - reflector(_View_.class, realView).mapRectFromViewToScreenCoords(rect, clipToParent); - int[] windowLocation = getWindowLocation(); - rect.offset(windowLocation[0], windowLocation[1]); - } - - // TODO(paulsowden): Should configure the correct frame on the ViewRootImpl instead and remove - // this. - private int[] getWindowLocation() { - int[] location = new int[2]; - LayoutParams rootParams = realView.getRootView().getLayoutParams(); - if (rootParams instanceof WindowManager.LayoutParams) { - location[0] = ((WindowManager.LayoutParams) rootParams).x; - location[1] = ((WindowManager.LayoutParams) rootParams).y; - } - return location; - } - @Implementation protected int getLayerType() { return this.layerType; @@ -891,12 +861,6 @@ void removeOnAttachStateChangeListener( void onScrollChanged(int l, int t, int oldl, int oldt); - @Direct - void getLocationOnScreen(int[] outLocation); - - @Direct - void mapRectFromViewToScreenCoords(RectF rect, boolean clipToParent); - @Direct int getSourceLayoutResId(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java index fe582460b6b..142a73f9ed1 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java @@ -1,313 +1,59 @@ package org.robolectric.shadows; -import static android.os.Build.VERSION_CODES.Q; import static android.os.Build.VERSION_CODES.R; import static android.os.Build.VERSION_CODES.S_V2; -import static org.robolectric.annotation.TextLayoutMode.Mode.REALISTIC; +import static android.os.Build.VERSION_CODES.TIRAMISU; import static org.robolectric.util.reflector.Reflector.reflector; -import android.content.res.Configuration; import android.graphics.Rect; -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.os.RemoteException; -import android.util.MergedConfiguration; import android.view.Display; import android.view.HandlerActionQueue; -import android.view.InsetsState; +import android.view.IWindow; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowManager; -import android.window.ClientWindowFrames; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Optional; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; -import org.robolectric.annotation.TextLayoutMode; -import org.robolectric.config.ConfigurationRegistry; -import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; -import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; -import org.robolectric.util.reflector.WithType; @Implements(value = ViewRootImpl.class, isInAndroidSdk = false) public class ShadowViewRootImpl { - private static final int RELAYOUT_RES_IN_TOUCH_MODE = 0x1; - - @RealObject protected ViewRootImpl realObject; - - /** - * The visibility of the system status bar. - * - *

The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing - * the current state via the returned {@link WindowInsets} instance if it has been set.. - * - *

NOTE: This state does not reflect the current state of system UI visibility flags or the - * current window insets. Rather it tracks the latest known state provided via {@link - * #setIsStatusBarVisible(boolean)}. - */ - private static Optional isStatusBarVisible = Optional.empty(); - - /** - * The visibility of the system navigation bar. - * - *

The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing - * the current state via the returned {@link WindowInsets} instance if it has been set. - * - *

NOTE: This state does not reflect the current state of system UI visibility flags or the - * current window insets. Rather it tracks the latest known state provided via {@link - * #setIsNavigationBarVisible(boolean)}. - */ - private static Optional isNavigationBarVisible = Optional.empty(); - - /** Allows other shadows to set the state of {@link #isStatusBarVisible}. */ - protected static void setIsStatusBarVisible(boolean isStatusBarVisible) { - ShadowViewRootImpl.isStatusBarVisible = Optional.of(isStatusBarVisible); - } - - /** Clears the last known state of {@link #isStatusBarVisible}. */ - protected static void clearIsStatusBarVisible() { - ShadowViewRootImpl.isStatusBarVisible = Optional.empty(); - } - - /** Allows other shadows to set the state of {@link #isNavigationBarVisible}. */ - protected static void setIsNavigationBarVisible(boolean isNavigationBarVisible) { - ShadowViewRootImpl.isNavigationBarVisible = Optional.of(isNavigationBarVisible); + static { + if (RuntimeEnvironment.getApiLevel() == R) { + ReflectionHelpers.setStaticField(ViewRootImpl.class, "sNewInsetsMode", 2); + } } - /** Clears the last known state of {@link #isNavigationBarVisible}. */ - protected static void clearIsNavigationBarVisible() { - ShadowViewRootImpl.isNavigationBarVisible = Optional.empty(); - } + @RealObject protected ViewRootImpl realObject; @Implementation public void playSoundEffect(int effectId) {} - @Implementation - protected int relayoutWindow( - WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) - throws RemoteException { - // TODO(christianw): probably should return WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED? - int result = 0; - if (ShadowWindowManagerGlobal.getInTouchMode() && RuntimeEnvironment.getApiLevel() <= S_V2) { - result |= RELAYOUT_RES_IN_TOUCH_MODE; - } - if (RuntimeEnvironment.getApiLevel() >= Q) { - // Simulate initializing the SurfaceControl member object, which happens during this method. - SurfaceControl surfaceControl = - reflector(ViewRootImplReflector.class, realObject).getSurfaceControl(); - ShadowSurfaceControl shadowSurfaceControl = Shadow.extract(surfaceControl); - shadowSurfaceControl.initializeNativeObject(); - } - return result; - } - + // TODO: Deprecate and remove this method, resize should get dispatched automatically to all + // windows added in the window session when a display changes its size. public void callDispatchResized() { - Optional> activityWindowInfoClass = - ReflectionHelpers.attemptLoadClass( - this.getClass().getClassLoader(), "android.window.ActivityWindowInfo"); - if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.UPSIDE_DOWN_CAKE - && activityWindowInfoClass.isPresent()) { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - - ClientWindowFrames frames = new ClientWindowFrames(); - // set the final field - ReflectionHelpers.setField(frames, "frame", frame); - final ClassParameter[] parameters = - new ClassParameter[] { - ClassParameter.from(ClientWindowFrames.class, frames), - ClassParameter.from(boolean.class, true), /* reportDraw */ - ClassParameter.from( - MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ - ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ - ClassParameter.from(boolean.class, false), /* forceLayout */ - ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ - ClassParameter.from(int.class, 0), /* displayId */ - ClassParameter.from(int.class, 0), /* syncSeqId */ - ClassParameter.from(boolean.class, false), /* dragResizing */ - ClassParameter.from( - activityWindowInfoClass.get(), - ReflectionHelpers.newInstance( - activityWindowInfoClass.get())) /* activityWindowInfo */ - }; - try { - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, realObject, "dispatchResized", parameters); - } catch (RuntimeException ex) { - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, - realObject, - "dispatchResized", - Arrays.copyOfRange(parameters, 0, parameters.length - 1)); - } - } else if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - - ClientWindowFrames frames = new ClientWindowFrames(); - // set the final field - ReflectionHelpers.setField(frames, "frame", frame); - - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, - realObject, - "dispatchResized", - ClassParameter.from(ClientWindowFrames.class, frames), - ClassParameter.from(boolean.class, true), /* reportDraw */ - ClassParameter.from( - MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ - ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ - ClassParameter.from(boolean.class, false), /* forceLayout */ - ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ - ClassParameter.from(int.class, 0), /* displayId */ - ClassParameter.from(int.class, 0), /* syncSeqId */ - ClassParameter.from(boolean.class, false) /* dragResizing */); - } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.S_V2) { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - - ClientWindowFrames frames = new ClientWindowFrames(); - // set the final field - ReflectionHelpers.setField(frames, "frame", frame); - - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, - realObject, - "dispatchResized", - ClassParameter.from(ClientWindowFrames.class, frames), - ClassParameter.from(boolean.class, true), /* reportDraw */ - ClassParameter.from( - MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ - ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ - ClassParameter.from(boolean.class, false), /* forceLayout */ - ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ - ClassParameter.from(int.class, 0), /* displayId */ - ClassParameter.from(int.class, 0), /* syncSeqId */ - ClassParameter.from(int.class, 0) /* resizeMode */); - } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.R) { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - - ClientWindowFrames frames = new ClientWindowFrames(); - // set the final field - ReflectionHelpers.setField(frames, "frame", frame); - - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, - realObject, - "dispatchResized", - ClassParameter.from(ClientWindowFrames.class, frames), - ClassParameter.from(boolean.class, true), /* reportDraw */ - ClassParameter.from( - MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ - ClassParameter.from(boolean.class, false), /* forceLayout */ - ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ - ClassParameter.from(int.class, 0) /* displayId */); - } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.Q) { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - - Rect emptyRect = new Rect(0, 0, 0, 0); - ReflectionHelpers.callInstanceMethod( - ViewRootImpl.class, - realObject, - "dispatchResized", - ClassParameter.from(Rect.class, frame), - ClassParameter.from(Rect.class, emptyRect), - ClassParameter.from(Rect.class, emptyRect), - ClassParameter.from(Rect.class, emptyRect), - ClassParameter.from(boolean.class, true), - ClassParameter.from(MergedConfiguration.class, new MergedConfiguration()), - ClassParameter.from(Rect.class, frame), - ClassParameter.from(boolean.class, false), - ClassParameter.from(boolean.class, false), - ClassParameter.from(int.class, 0), - ClassParameter.from( - android.view.DisplayCutout.ParcelableWrapper.class, - new android.view.DisplayCutout.ParcelableWrapper())); - } else { - Display display = getDisplay(); - Rect frame = new Rect(); - display.getRectSize(frame); - reflector(ViewRootImplReflector.class, realObject).dispatchResized(frame); - } + ShadowWindowManagerGlobal.notifyResize( + reflector(ViewRootImplReflector.class, realObject).getWindow()); } protected Display getDisplay() { return reflector(ViewRootImplReflector.class, realObject).getDisplay(); } - @Implementation - protected void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { - reflector(ViewRootImplReflector.class, realObject).setView(view, attrs, panelParentView); - if (ConfigurationRegistry.get(TextLayoutMode.Mode.class) == REALISTIC) { - Rect winFrame = new Rect(); - getDisplay().getRectSize(winFrame); - reflector(ViewRootImplReflector.class, realObject).setWinFrame(winFrame); - } - } - - @Implementation(minSdk = R) - protected void setView( - View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) { - reflector(ViewRootImplReflector.class, realObject) - .setView(view, attrs, panelParentView, userId); - if (ConfigurationRegistry.get(TextLayoutMode.Mode.class) == REALISTIC) { - Rect winFrame = new Rect(); - getDisplay().getRectSize(winFrame); - reflector(ViewRootImplReflector.class, realObject).setWinFrame(winFrame); - } - } - - /** - * On Android R+ {@link WindowInsets} supports checking visibility of specific inset types. - * - *

For those SDK levels, override the real {@link WindowInsets} with the tracked system bar - * visibility status ({@link #isStatusBarVisible}/{@link #isNavigationBarVisible}), if set. - * - *

NOTE: We use state tracking in place of a longer term solution of implementing the insets - * calculations and broadcast (via listeners) for now. Once we have insets calculations working we - * should remove this mechanism. - */ - @Implementation(minSdk = R) - protected WindowInsets getWindowInsets(boolean forceConstruct) { - WindowInsets realInsets = - reflector(ViewRootImplReflector.class, realObject).getWindowInsets(forceConstruct); - - WindowInsets.Builder overridenInsetsBuilder = new WindowInsets.Builder(realInsets); - - if (isStatusBarVisible.isPresent()) { - overridenInsetsBuilder = - overridenInsetsBuilder.setVisible( - WindowInsets.Type.statusBars(), isStatusBarVisible.get()); - } - - if (isNavigationBarVisible.isPresent()) { - overridenInsetsBuilder = - overridenInsetsBuilder.setVisible( - WindowInsets.Type.navigationBars(), isNavigationBarVisible.get()); - } - - return overridenInsetsBuilder.build(); - } + @Implementation(minSdk = TIRAMISU) + protected void updateBlastSurfaceIfNeeded() {} @Resetter public static void reset() { @@ -316,9 +62,6 @@ public static void reset() { viewRootImplStatic.setFirstDrawHandlers(new ArrayList<>()); viewRootImplStatic.setFirstDrawComplete(false); viewRootImplStatic.setConfigCallbacks(new ArrayList<>()); - - clearIsStatusBarVisible(); - clearIsNavigationBarVisible(); } public void callWindowFocusChanged(boolean hasFocus) { @@ -337,6 +80,8 @@ Surface getSurface() { /** Reflector interface for {@link ViewRootImpl}'s internals. */ @ForType(ViewRootImpl.class) protected interface ViewRootImplReflector { + @Accessor("mWindow") + IWindow getWindow(); @Direct void setView(View view, WindowManager.LayoutParams attrs, View panelParentView); @@ -379,116 +124,6 @@ protected interface ViewRootImplReflector { @Accessor("mWindowAttributes") WindowManager.LayoutParams getWindowAttributes(); - // <= LOLLIPOP_MR1 - void dispatchResized( - Rect frame, - Rect overscanInsets, - Rect contentInsets, - Rect visibleInsets, - Rect stableInsets, - boolean reportDraw, - Configuration newConfig); - - // <= M - void dispatchResized( - Rect frame, - Rect overscanInsets, - Rect contentInsets, - Rect visibleInsets, - Rect stableInsets, - Rect outsets, - boolean reportDraw, - Configuration newConfig); - - // <= N_MR1 - void dispatchResized( - Rect frame, - Rect overscanInsets, - Rect contentInsets, - Rect visibleInsets, - Rect stableInsets, - Rect outsets, - boolean reportDraw, - Configuration newConfig, - Rect backDropFrame, - boolean forceLayout, - boolean alwaysConsumeNavBar); - - // <= O_MR1 - void dispatchResized( - Rect frame, - Rect overscanInsets, - Rect contentInsets, - Rect visibleInsets, - Rect stableInsets, - Rect outsets, - boolean reportDraw, - @WithType("android.util.MergedConfiguration") Object mergedConfiguration, - Rect backDropFrame, - boolean forceLayout, - boolean alwaysConsumeNavBar, - int displayId); - - // >= P - void dispatchResized( - Rect frame, - Rect overscanInsets, - Rect contentInsets, - Rect visibleInsets, - Rect stableInsets, - Rect outsets, - boolean reportDraw, - @WithType("android.util.MergedConfiguration") Object mergedConfiguration, - Rect backDropFrame, - boolean forceLayout, - boolean alwaysConsumeNavBar, - int displayId, - @WithType("android.view.DisplayCutout$ParcelableWrapper") Object displayCutout); - - default void dispatchResized(Rect frame) { - Rect emptyRect = new Rect(0, 0, 0, 0); - - int apiLevel = RuntimeEnvironment.getApiLevel(); - if (apiLevel <= Build.VERSION_CODES.LOLLIPOP_MR1) { - dispatchResized(frame, emptyRect, emptyRect, emptyRect, emptyRect, true, null); - } else if (apiLevel <= Build.VERSION_CODES.M) { - dispatchResized(frame, emptyRect, emptyRect, emptyRect, emptyRect, emptyRect, true, null); - } else if (apiLevel <= Build.VERSION_CODES.N_MR1) { - dispatchResized( - frame, emptyRect, emptyRect, emptyRect, emptyRect, emptyRect, true, null, frame, false, - false); - } else if (apiLevel <= Build.VERSION_CODES.O_MR1) { - dispatchResized( - frame, - emptyRect, - emptyRect, - emptyRect, - emptyRect, - emptyRect, - true, - new MergedConfiguration(), - frame, - false, - false, - 0); - } else { // apiLevel >= Build.VERSION_CODES.P - dispatchResized( - frame, - emptyRect, - emptyRect, - emptyRect, - emptyRect, - emptyRect, - true, - new MergedConfiguration(), - frame, - false, - false, - 0, - new android.view.DisplayCutout.ParcelableWrapper()); - } - } - // SDK <= S_V2 void windowFocusChanged(boolean hasFocus, boolean inTouchMode); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java index 47467bb172a..384ece475ff 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java @@ -1,11 +1,31 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; +import static android.os.Build.VERSION_CODES.M; +import static android.os.Build.VERSION_CODES.N; +import static android.os.Build.VERSION_CODES.N_MR1; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static android.os.Build.VERSION_CODES.S; +import static android.os.Build.VERSION_CODES.S_V2; +import static android.os.Build.VERSION_CODES.TIRAMISU; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowInsets.Type.systemBars; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.max; import static java.lang.Math.round; +import static java.util.Arrays.stream; import static org.robolectric.shadows.ShadowView.useRealGraphics; +import static org.robolectric.shadows.SystemUi.systemUiForDisplay; +import static org.robolectric.util.ReflectionHelpers.callConstructor; +import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; import static org.robolectric.util.reflector.Reflector.reflector; import android.annotation.FloatRange; @@ -13,38 +33,64 @@ import android.app.Instrumentation; import android.content.ClipData; import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; +import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; +import android.util.MergedConfiguration; +import android.view.DisplayCutout; +import android.view.DisplayInfo; +import android.view.Gravity; +import android.view.IWindow; import android.view.IWindowManager; import android.view.IWindowSession; +import android.view.InsetsSource; +import android.view.InsetsSourceControl; +import android.view.InsetsState; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; import android.view.WindowManagerGlobal; +import android.view.WindowRelayoutResult; +import android.window.ActivityWindowInfo; import android.window.BackEvent; import android.window.BackMotionEvent; +import android.window.ClientWindowFrames; import android.window.OnBackInvokedCallbackInfo; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.Closeable; +import java.lang.reflect.Array; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map.Entry; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.ClassName; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowInsetsState.InsetsStateReflector; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Constructor; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.Static; -import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link WindowManagerGlobal}. */ @SuppressWarnings("unused") // Unused params are implementations of Android SDK methods. @@ -72,6 +118,10 @@ static void setInTouchMode(boolean inTouchMode) { windowSessionDelegate.setInTouchMode(inTouchMode); } + static void notifyResize(IWindow window) { + windowSessionDelegate.sendResize(window); + } + /** * Returns the last {@link ClipData} passed to a drag initiated from a call to {@link * View#startDrag} or {@link View#startDragAndDrop}, or null if there isn't one. @@ -296,7 +346,12 @@ protected static synchronized IWindowSession getWindowSession() { case "add": // SDK 16 case "addToDisplay": // SDK 17-29 case "addToDisplayAsUser": // SDK 30+ - return windowSessionDelegate.getAddFlags(); + return windowSessionDelegate.addToDisplay(args); + case "remove": + windowSessionDelegate.remove(args); + return null; + case "relayout": + return windowSessionDelegate.relayout(args); case "getInTouchMode": return windowSessionDelegate.getInTouchMode(); case "performDrag": @@ -313,6 +368,11 @@ protected static synchronized IWindowSession getWindowSession() { case "reportSystemGestureExclusionChanged": windowSessionDelegate.systemGestureExclusionRects = (List) args[1]; return null; + case "insetsModified": + case "updateRequestedVisibilities": + case "updateRequestedVisibleTypes": + windowSessionDelegate.updateInsets(args); + return null; default: return ReflectionHelpers.defaultValueForType( method.getReturnType().getName()); @@ -358,9 +418,12 @@ interface WindowManagerGlobalReflector { } private static class WindowSessionDelegate { + private final LinkedHashMap windows = new LinkedHashMap<>(); + // From WindowManagerGlobal (was WindowManagerImpl in JB). static final int ADD_FLAG_IN_TOUCH_MODE = 0x1; static final int ADD_FLAG_APP_VISIBLE = 0x2; + static final int RELAYOUT_RES_IN_TOUCH_MODE = 0x1; // TODO: Default to touch mode always. private boolean inTouchMode = useRealGraphics(); @@ -369,7 +432,36 @@ private static class WindowSessionDelegate { @Nullable private List systemGestureExclusionRects; @Nullable private PredictiveBackGesture currentPredictiveBackGesture; - protected int getAddFlags() { + protected int addToDisplay(Object[] args) { + int sdk = RuntimeEnvironment.getApiLevel(); + WindowInfo windowInfo = windows.computeIfAbsent((IWindow) args[0], id -> new WindowInfo()); + int displayId = (int) args[sdk <= R ? 4 : 3]; + // TODO: This is to allow window insets to be keyed per display, i.e. a window has requested + // insets visibility changed before it was added to a display, does android actually allow + // per display window inset visibilities? + if (sdk >= R) { + applyInsets(displayId, windowInfo.requestedVisibleTypes); + } + windowInfo.displayId = displayId; + + // Create some insets source controls otherwise the insets controller will not apply changes. + if (sdk >= R && sdk < UPSIDE_DOWN_CAKE) { + populateInsetSourceControls(windowInfo, findFirst(InsetsSourceControl[].class, args)); + windowInfo.hasInsetsControl = true; + transferWindowInsetsControlTo(windowInfo); + } + Rect[] rects = findAll(Rect.class, args); + int rectIdx = 0; + configureWindowFrames( + windowInfo, + /* inAttrs= */ (WindowManager.LayoutParams) args[sdk <= R ? 2 : 1], + /* requestedSize= */ null, + /* outFrame= */ sdk >= P && rects.length > rectIdx ? rects[rectIdx++] : null, + /* outContentInsets= */ sdk <= R ? rects[rectIdx++] : null, + /* outVisibleInsets= */ null, + /* outStableInsets= */ sdk >= LOLLIPOP_MR1 && sdk <= R ? rects[rectIdx] : null, + /* outInsetsState= */ sdk >= Q ? findFirst(InsetsState.class, args) : null); + int res = 0; // Temporarily enable this based on a system property to allow for test migration. This will // eventually be updated to default and true and eventually removed, Robolectric's previous @@ -379,12 +471,123 @@ protected int getAddFlags() { || "true".equals(System.getProperty("robolectric.areWindowsMarkedVisible", "false"))) { res |= ADD_FLAG_APP_VISIBLE; } - if (getInTouchMode()) { - res |= ADD_FLAG_IN_TOUCH_MODE; - } + res |= inTouchMode ? ADD_FLAG_IN_TOUCH_MODE : 0; return res; } + protected void remove(Object[] args) { + IWindow window = (IWindow) args[0]; + windows.remove(window); + // TODO: This transfers control to the last window, should there be another heuristic here? + // TODO: Streams.findLast is not available in Android Guava yet. + transferWindowInsetsControlTo(windows.values().stream().reduce((a, b) -> b).orElse(null)); + } + + protected int relayout(Object[] args) { + int sdk = RuntimeEnvironment.getApiLevel(); + WindowRelayoutResult windowLayoutResult = + sdk >= VANILLA_ICE_CREAM ? findFirst(WindowRelayoutResult.class, args) : null; + + // Simulate initializing the SurfaceControl member object, which happens during this method. + if (sdk >= Q) { + SurfaceControl surfaceControl = + sdk >= VANILLA_ICE_CREAM + ? windowLayoutResult.surfaceControl + : findFirst(SurfaceControl.class, args); + Shadow.extract(surfaceControl).initializeNativeObject(); + } + + IWindow window = (IWindow) args[0]; + WindowInfo windowInfo = windows.get(window); + // In legacy looper mode relayout can be called out of order with add so just ignore it. + // TODO: In paused looper mode the material SnackbarManager static instance leaks state + // between tests and triggers relayout on window roots that are cleared, for now just ignore + // them here, but ideally this library would not leak state between tests. + if (windowInfo != null) { + if (sdk >= R && sdk < UPSIDE_DOWN_CAKE) { + InsetsSourceControl[] controls = findFirst(InsetsSourceControl[].class, args); + if (windowInfo.hasInsetsControl) { + populateInsetSourceControls(windowInfo, controls); + } else { + Arrays.setAll(controls, i -> null); + } + } + Rect[] rects = findAll(Rect.class, args); + int requestedSizeIdx = sdk < S ? 3 : 2; + configureWindowFrames( + checkNotNull(windowInfo), + /* inAttrs= */ (WindowManager.LayoutParams) args[sdk <= R ? 2 : 1], + /* requestedSize= */ new Point( + (int) args[requestedSizeIdx], (int) args[requestedSizeIdx + 1]), + /* outFrame= */ rects.length > 0 + ? rects[0] + : (windowLayoutResult != null + ? windowLayoutResult.frames + : findFirst(ClientWindowFrames.class, args)) + .frame, + /* outContentInsets= */ sdk <= R ? rects[2] : null, + /* outVisibleInsets= */ sdk <= R ? rects[3] : null, + /* outStableInsets= */ sdk <= R ? rects[4] : null, + /* outInsetsState= */ sdk >= Q + ? (windowLayoutResult != null + ? windowLayoutResult.insetsState + : findFirst(InsetsState.class, args)) + : null); + } + + return inTouchMode ? RELAYOUT_RES_IN_TOUCH_MODE : 0; + } + + private void configureWindowFrames( + WindowInfo windowInfo, + @Nullable WindowManager.LayoutParams inAttrs, + Point requestedSize, + Rect outFrame, + Rect outContentInsets, + Rect outVisibleInsets, + Rect outStableInsets, + InsetsState outInsetsState) { + SystemUi systemUi = systemUiForDisplay(windowInfo.displayId); + DisplayInfo displayInfo = + DisplayManagerGlobal.getInstance().getDisplayInfo(windowInfo.displayId); + WindowManager.LayoutParams attrs = windowInfo.attrs; + if (inAttrs != null) { + attrs.copyFrom(inAttrs); + } + windowInfo.displayFrame.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight); + Rect contentFrame = new Rect(windowInfo.displayFrame); + systemUi.adjustFrameForInsets(attrs, contentFrame); + // TODO: Remove this and respect the requested size as real Android does. For back compat + // reasons temporarily ignore requested size. + boolean useRequestedSize = Boolean.getBoolean("robolectric.windowManager.useRequestedSize"); + int width = + useRequestedSize && requestedSize != null && attrs.width != LayoutParams.MATCH_PARENT + ? requestedSize.x + : (attrs.width > 0 ? attrs.width : contentFrame.width()); + int height = + useRequestedSize && requestedSize != null && attrs.height != LayoutParams.MATCH_PARENT + ? requestedSize.y + : (attrs.height > 0 ? attrs.height : contentFrame.height()); + // TODO: Take account of parent frame for child windows. + Gravity.apply( + attrs.gravity, + width, + height, + contentFrame, + (int) (attrs.x + attrs.horizontalMargin * contentFrame.width()), + (int) (attrs.y + attrs.verticalMargin * contentFrame.height()), + windowInfo.frame); + if (!useRequestedSize) { + // If we are not respecting the requested size, for backwards compatibility allow the window + // to offset to the requested position ignoring the gravity and display bounds. + windowInfo.frame.offsetTo(attrs.x, attrs.y); + } else { + Gravity.applyDisplay(attrs.gravity, contentFrame, windowInfo.frame); + } + systemUiForDisplay(windowInfo.displayId).putInsets(windowInfo); + windowInfo.put(outFrame, outContentInsets, outVisibleInsets, outStableInsets, outInsetsState); + } + public boolean getInTouchMode() { return inTouchMode; } @@ -408,6 +611,264 @@ public Object performDrag(Object[] args) { } throw new AssertionError("Missing ClipData param"); } + + public void updateInsets(Object[] args) { + int sdk = RuntimeEnvironment.getApiLevel(); + checkState(sdk >= R); + IWindow window = (IWindow) args[0]; + WindowInfo windowInfo = windows.computeIfAbsent(window, id -> new WindowInfo()); + if (sdk <= S) { + InsetsState state = (InsetsState) args[1]; + InsetsSource statusBar = state.peekSource(ShadowInsetsState.STATUS_BARS); + InsetsSource navBar = state.peekSource(ShadowInsetsState.NAVIGATION_BARS); + windowInfo.requestedVisibleTypes = + (statusBar == null || statusBar.isVisible() ? statusBars() : 0) + | (navBar == null || navBar.isVisible() ? navigationBars() : 0); + } else if (sdk <= TIRAMISU) { + InsetsVisibilitiesReflector visibilities = + reflector(InsetsVisibilitiesReflector.class, args[1]); + boolean statusBar = visibilities.getVisibility(ShadowInsetsState.STATUS_BARS); + boolean navBar = visibilities.getVisibility(ShadowInsetsState.NAVIGATION_BARS); + windowInfo.requestedVisibleTypes = + (statusBar ? statusBars() : 0) | (navBar ? navigationBars() : 0); + } else { + windowInfo.requestedVisibleTypes = (int) args[1]; + } + if (windowInfo.displayId != -1) { + applyInsets(windowInfo.displayId, windowInfo.requestedVisibleTypes); + } + } + + void applyInsets(int displayId, int requestedVisibleTypes) { + checkState(displayId != -1); + SystemUi systemUi = systemUiForDisplay(displayId); + boolean statusBarVisible = (requestedVisibleTypes & statusBars()) != 0; + if (systemUi.getStatusBar().isVisible() != statusBarVisible) { + systemUi.getStatusBar().setVisible(statusBarVisible); + notifyInsetsChanges(systemUi, systemUi.getStatusBar().getId()); + } + boolean navBarVisible = (requestedVisibleTypes & navigationBars()) != 0; + if (systemUi.getNavigationBar().isVisible() != navBarVisible) { + systemUi.getNavigationBar().setVisible(navBarVisible); + notifyInsetsChanges(systemUi, systemUi.getNavigationBar().getId()); + } + } + + void notifyInsetsChanges(SystemUi systemUi, @Nullable Integer type) { + for (Entry windowEntry : windows.entrySet()) { + if (windowEntry.getValue().displayId == systemUi.getDisplayId()) { + systemUi.putInsets(windowEntry.getValue()); + sendInsetsControlChanged(windowEntry.getKey(), type, false); + // TODO: only send resize if the window resized. + sendResize(windowEntry.getKey()); + } + } + } + + void sendInsetsControlChanged( + IWindow window, @Nullable Integer type, boolean hasControlsChanged) { + int sdk = RuntimeEnvironment.getApiLevel(); + WindowInfo windowInfo = checkNotNull(windows.get(window)); + InsetsState insetsState = new InsetsState(windowInfo.insetsState); + // On R if we don't remove the sources that aren't changing we'll infinite loop when toggling + // visibility of multiple bars. + if (sdk == R && type != null) { + for (int i = 0; i < Shadow.extract(insetsState).getSourceSize(); i++) { + if (i != type) { + insetsState.removeSource(i); + } + } + } + if ((sdk == R && !hasControlsChanged) || sdk >= S && sdk <= S_V2) { + ClassParameterBuilder params = new ClassParameterBuilder(); + params.add(InsetsState.class, windowInfo.insetsState); + /* willMove */ params.addIf(sdk >= S, boolean.class, false); + /* willResize */ params.addIf(sdk >= S, boolean.class, false); + callInstanceMethod(window, "insetsChanged", params.build()); + } else { + ClassParameterBuilder params = new ClassParameterBuilder(); + params.add(InsetsState.class, windowInfo.insetsState); + // TODO: We should give control to the active window. + if (sdk >= VANILLA_ICE_CREAM) { + params.add(InsetsSourceControl.Array.class, new InsetsSourceControl.Array()); + } else { + params.add( + InsetsSourceControl[].class, + windowInfo.hasInsetsControl ? populateInsetSourceControls(windowInfo, null) : null); + } + callInstanceMethod(window, "insetsControlChanged", params.build()); + } + } + + void sendResize(IWindow window) { + int sdk = RuntimeEnvironment.getApiLevel(); + WindowInfo windowInfo = checkNotNull(windows.get(window)); + configureWindowFrames( + windowInfo, + windowInfo.attrs, + /* requestedSize= */ null, + /* outFrame= */ null, + /* outContentInsets= */ null, + /* outVisibleInsets= */ null, + /* outStableInsets= */ null, + /* outInsetsState= */ null); + Configuration configuration = + RuntimeEnvironment.getApplication().getResources().getConfiguration(); + ClassParameterBuilder args = new ClassParameterBuilder(); + + // The resized method has changed pretty much every other release, this is a canonicalize-d + // set of all the parameters it has ever taken. + if (sdk >= S) { + /* frames */ args.add(ClientWindowFrames.class, windowInfo.frames); + } else { + /* frame */ args.add(Rect.class, windowInfo.frame); + /* overscanInsets */ args.addIf(sdk <= Q, Rect.class, new Rect()); + /* contentInsets */ args.add(Rect.class, windowInfo.contentInsets); + /* visibleInsets */ args.add(Rect.class, windowInfo.visibleInsets); + /* stableInsets */ args.add(Rect.class, windowInfo.stableInsets); + /* outsets */ args.addIf(sdk >= M && sdk <= Q, Rect.class, new Rect()); + } + /* reportDraw */ args.add(boolean.class, false); + if (sdk <= N_MR1) { + /* newConfig */ args.add(Configuration.class, configuration); + } else { + /* newMergedConfiguration */ args.add( + MergedConfiguration.class, new MergedConfiguration(configuration)); + } + /* backDropFrame */ args.addIf(sdk >= N && sdk <= R, Rect.class, new Rect()); + if (sdk >= TIRAMISU) { + /* insetsState */ args.add(InsetsState.class, windowInfo.insetsState); + } + /* forceLayout */ args.addIf(sdk >= N, boolean.class, false); + /* alwaysConsumeNavBar */ args.addIf(sdk >= N, boolean.class, false); + /* displayId */ args.addIf(sdk >= O, int.class, windowInfo.displayId); + if (sdk >= P && sdk <= R) { + /* displayCutout */ args.add( + DisplayCutout.ParcelableWrapper.class, new DisplayCutout.ParcelableWrapper()); + } + /* syncSeqId */ args.addIf(sdk >= TIRAMISU, int.class, 0); + /* resizeMode */ args.addIf(sdk == TIRAMISU, int.class, 0); + /* dragResizing */ args.addIf(sdk >= UPSIDE_DOWN_CAKE, boolean.class, false); + if (sdk > UPSIDE_DOWN_CAKE) { + /* activityWindowInfo */ args.add(ActivityWindowInfo.class, null); + } + callInstanceMethod(window, "resized", args.build()); + } + + private void transferWindowInsetsControlTo(WindowInfo windowInfo) { + // If we don't transfer the controls on R then windows conflict when their insets mismatch, + // resulting in infinite loops, and if no window has control then insets are not updated. + // TODO: This is almost certainly not the correct logic for determining which window has + // control. + if (RuntimeEnvironment.getApiLevel() != R) { + return; + } + for (Entry entry : windows.entrySet()) { + boolean hasControl = entry.getValue() == windowInfo; + if (entry.getValue().hasInsetsControl != hasControl) { + entry.getValue().hasInsetsControl = hasControl; + sendInsetsControlChanged(entry.getKey(), null, true); + } + } + } + + @CanIgnoreReturnValue + private InsetsSourceControl[] populateInsetSourceControls( + WindowInfo windowInfo, InsetsSourceControl[] controls) { + int sdk = RuntimeEnvironment.getApiLevel(); + // Skip bars after IME as they have the same public types as navigation/status bars and + // their visibility combines. + int lastControl = reflector(InsetsStateReflector.class).getImeType(); + if (controls == null) { + controls = new InsetsSourceControl[lastControl + 1]; + } + for (int i = 0; i <= lastControl; i++) { + ClassParameterBuilder params = new ClassParameterBuilder(); + /* type */ params.add(int.class, i); + /* leash */ params.add(SurfaceControl.class, null); + /* surfacePosition */ params.add(Point.class, new Point()); + /* insetsHint */ params.addIf(sdk >= S, Insets.class, Insets.of(0, 0, 0, 0)); + controls[i] = callConstructor(InsetsSourceControl.class, params.build()); + // Populate the same insets as we did controls, otherwise the insets controller can + // infinite loop as it sees the insets being added and removed every time. + Shadow.extract(windowInfo.insetsState).getOrCreateSource(i); + } + return controls; + } + } + + static final class WindowInfo { + final Rect displayFrame = new Rect(); + final ClientWindowFrames frames; + final Rect frame; + final InsetsState insetsState = + RuntimeEnvironment.getApiLevel() >= Q ? new InsetsState() : null; + final Rect contentInsets = new Rect(); + final Rect visibleInsets = new Rect(); + final Rect stableInsets = new Rect(); + final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(); + int displayId = -1; + int requestedVisibleTypes = RuntimeEnvironment.getApiLevel() >= R ? systemBars() : 0; + boolean hasInsetsControl; + + WindowInfo() { + if (RuntimeEnvironment.getApiLevel() >= S) { + frames = new ClientWindowFrames(); + frame = frames.frame; + } else { + frames = null; + frame = new Rect(); + } + } + + void put( + Rect outFrame, + Rect outContentInsets, + Rect outVisibleInsets, + Rect outStableInsets, + InsetsState outInsetsState) { + if (outFrame != null) { + outFrame.set(frame); + } + if (outContentInsets != null) { + outContentInsets.set(contentInsets); + } + if (outVisibleInsets != null) { + outVisibleInsets.set(visibleInsets); + } + if (outStableInsets != null) { + outStableInsets.set(stableInsets); + } + if (outInsetsState != null) { + outInsetsState.set(insetsState, /* copySources= */ true); + } + } + } + + private static T findFirst(Class type, Object[] args) { + return type.cast(stream(args).filter(type::isInstance).findFirst().get()); + } + + private static T[] findAll(Class type, Object[] args) { + return stream(args).filter(type::isInstance).toArray(len -> (T[]) Array.newInstance(type, len)); + } + + private static final class ClassParameterBuilder { + private final List> parameters = new ArrayList<>(); + + void add(Class type, T parameter) { + parameters.add(ClassParameter.from(type, parameter)); + } + + void addIf(boolean shouldAdd, Class type, T parameter) { + if (shouldAdd) { + add(type, parameter); + } + } + + ClassParameter[] build() { + return parameters.toArray(new ClassParameter[0]); + } } @ForType(BackMotionEvent.class) @@ -444,12 +905,17 @@ BackMotionEvent newBackMotionEventPostV( RemoteAnimationTarget departingAnimationTarget); } + @ForType(className = "android.view.InsetsVisibilities") + interface InsetsVisibilitiesReflector { + boolean getVisibility(int type); + } + private static class BackMotionEvents { private BackMotionEvents() {} static BackMotionEvent newBackMotionEvent( @BackEvent.SwipeEdge int edge, float touchX, float touchY, float progress) { - if (RuntimeEnvironment.getApiLevel() < V.SDK_INT) { + if (RuntimeEnvironment.getApiLevel() < VANILLA_ICE_CREAM) { return reflector(BackMotionEventReflector.class) .newBackMotionEvent( touchX, touchY, progress, 0f, // velocity x diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerImpl.java index bf54a5c140c..5620954095c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerImpl.java @@ -10,6 +10,7 @@ import android.content.Context; import android.graphics.Insets; import android.graphics.Rect; +import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayCutout; import android.view.InsetsState; @@ -82,13 +83,13 @@ && reflector(ViewRootImplReflector.class).getNewInsetsMode() == NEW_INSETS_MODE_ insetsState, "calculateInsets", ClassParameter.from(Rect.class, bounds), - null, + ClassParameter.from(InsetsState.class, null), ClassParameter.from(Boolean.TYPE, isScreenRound), ClassParameter.from(Boolean.TYPE, alwaysConsumeSystemBars), - ClassParameter.from(DisplayCutout.ParcelableWrapper.class, displayCutout.get()), + ClassParameter.from(DisplayCutout.class, displayCutout.get()), ClassParameter.from(int.class, SOFT_INPUT_ADJUST_NOTHING), ClassParameter.from(int.class, SYSTEM_UI_FLAG_VISIBLE), - null); + ClassParameter.from(SparseIntArray.class, null)); } else { return new WindowInsets.Builder() .setAlwaysConsumeSystemBars(alwaysConsumeSystemBars) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/SystemUi.java b/shadows/framework/src/main/java/org/robolectric/shadows/SystemUi.java new file mode 100644 index 00000000000..49711b38302 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/SystemUi.java @@ -0,0 +1,534 @@ +package org.robolectric.shadows; + +import static android.os.Build.VERSION_CODES.R; +import static java.lang.Math.max; +import static java.lang.Math.round; + +import android.graphics.Rect; +import android.hardware.display.DisplayManagerGlobal; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.InsetsState; +import android.view.Surface; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowInfo; +import org.robolectric.shadows.SystemUi.SystemBar.Side; + +/** + * State holder for the Android system UI. + * + *

The system UI is configured per display and the system UI can be retrieved for the default + * display using {@link #systemUiForDefaultDisplay()} or for a display identified by its ID using + * {@link #systemUiForDisplay(int)}. + * + *

For backwards compatibility with previous Robolectric versions by default the system UIs are + * configured with no status bar or navigation insets, to apply a "standard" phone setup configure a + * status bar and navigation bar behavior e.g. in your test setup: + * + *

{@code
+ * systemUiForDefaultDisplay()
+ *     .setBehavior(SystemUi.STANDARD_STATUS_BAR, SystemUi.GESTURAL_NAVIGATION);
+ * }
+ * + *

{@link SystemUi} includes the most common Android system UI behaviors including: + * + *

    + *
  • {@link #NO_STATUS_BAR} - The default, no status bar insets reserved. + *
  • {@link #STANDARD_STATUS_BAR} - A standard status bar that grows if a top cutout is present. + *
  • {@link #GESTURAL_NAVIGATION} - Standard gestural navigation with bottom inset and gestural + * areas on the bottom and sides of the screen. + *
  • {@link #THREE_BUTTON_NAVIGATION} - Standard three button navigation bar that aligns to the + * bottom of the screen, and on smaller screens moves to the sides when rotated. + *
  • {@link #GESTURAL_NAVIGATION} - Standard two button navigation bar with similar alignment to + * the three button bar but also reserves a gestural area at the bottom of the screen. + *
+ * + *

It's recommended to use the predefined behaviors which attempt to align with real Android + * behavior, but if necessary custom system bar and navigation bar behaviors can be defined by + * implementing the {@link StatusBarBehavior} and {@link NavigationBarBehavior} interfaces + * respectively. + */ +// TODO: Make public when we're happy with the implementation/api/behavior +final class SystemUi { + /** Default status bar behavior which renders a 0 height status bar. */ + public static final StatusBarBehavior NO_STATUS_BAR = new NoStatusBarBehavior(); + + /** Standard Android status bar behavior which behaves similarly to real Android. */ + public static final StatusBarBehavior STANDARD_STATUS_BAR = new StandardStatusBarBehavior(); + + /** Default navigation bar behavior which renders a 0 height navigation bar. */ + public static final NavigationBarBehavior NO_NAVIGATION_BAR = new NoNavigationBarBehavior(); + + /** Standard Android gestural navigation bar behavior. */ + public static final NavigationBarBehavior GESTURAL_NAVIGATION = + new GesturalNavigationBarBehavior(); + + /** Standard Android three button navigation bar behavior. */ + public static final NavigationBarBehavior THREE_BUTTON_NAVIGATION = + new ButtonNavigationBarBehavior(); + + private final int displayId; + private final StatusBar statusBar; + private final NavigationBar navigationBar; + private final ImmutableList systemsBars; + + interface OnChangeListener { + void onChange(); + } + + private final List listeners = new ArrayList<>(); + + /** Returns the {@link SystemUi} for the default display. */ + public static SystemUi systemUiForDefaultDisplay() { + return systemUiForDisplay(Display.DEFAULT_DISPLAY); + } + + /** Returns the {@link SystemUi} for the given display. */ + public static SystemUi systemUiForDisplay(int displayId) { + return Shadow.extract(DisplayManagerGlobal.getInstance()) + .getSystemUi(displayId); + } + + SystemUi(int displayId) { + this.displayId = displayId; + statusBar = new StatusBar(displayId); + navigationBar = new NavigationBar(displayId); + systemsBars = ImmutableList.of(statusBar, navigationBar); + } + + int getDisplayId() { + return displayId; + } + + void addListener(OnChangeListener listener) { + listeners.add(listener); + } + + public StatusBar getStatusBar() { + return statusBar; + } + + /** Returns the status bar behavior. The default status bar behavior is {@link #NO_STATUS_BAR}. */ + public StatusBarBehavior getStatusBarBehavior() { + return statusBar.getBehavior(); + } + + /** + * Sets the status bar behavior. + * + *

The default behavior is {@link #NO_STATUS_BAR}, use {@link #STANDARD_STATUS_BAR} for a + * standard Android status bar behavior. + */ + public void setStatusBarBehavior(StatusBarBehavior statusBarBehavior) { + statusBar.setBehavior(statusBarBehavior); + } + + public NavigationBar getNavigationBar() { + return navigationBar; + } + + /** + * Returns the navigation bar behavior. The default navigation bar behavior is {@link + * #NO_NAVIGATION_BAR}. + */ + public NavigationBarBehavior getNavigationBarBehavior() { + return navigationBar.getBehavior(); + } + + /** + * Sets the navigation bar behavior. + * + *

The default behavior is {@link #NO_NAVIGATION_BAR}, use {@link #GESTURAL_NAVIGATION} or + * {@link #THREE_BUTTON_NAVIGATION} for a standard on screen Android navigation bar behavior. + */ + public void setNavigationBarBehavior(NavigationBarBehavior statusBarBehavior) { + navigationBar.setBehavior(statusBarBehavior); + } + + public void setBehavior( + StatusBarBehavior statusBarBehavior, NavigationBarBehavior navigationBarBehavior) { + setStatusBarBehavior(statusBarBehavior); + setNavigationBarBehavior(navigationBarBehavior); + } + + @SuppressWarnings("deprecation") // Back compat support for system ui visibility + void adjustFrameForInsets(WindowManager.LayoutParams attrs, Rect outFrame) { + boolean hideStatusBar; + boolean hideNavigationBar; + if (RuntimeEnvironment.getApiLevel() >= R) { + hideStatusBar = (attrs.getFitInsetsTypes() & WindowInsets.Type.statusBars()) != 0; + hideNavigationBar = (attrs.getFitInsetsTypes() & WindowInsets.Type.navigationBars()) != 0; + } else { + int systemUiVisibility = attrs.systemUiVisibility | attrs.subtreeSystemUiVisibility; + hideStatusBar = + (systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 + && (attrs.flags & WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN) == 0 + && (attrs.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) == 0; + hideNavigationBar = + (systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 + && (attrs.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) == 0; + } + if (hideStatusBar) { + statusBar.insetFrame(outFrame); + } + if (hideNavigationBar) { + navigationBar.insetFrame(outFrame); + } + } + + void putInsets(WindowInfo windowInfo) { + putInsets(windowInfo, windowInfo.contentInsets, /* includeNotVisible= */ false); + putInsets(windowInfo, windowInfo.visibleInsets, /* includeNotVisible= */ false); + putInsets(windowInfo, windowInfo.stableInsets, /* includeNotVisible= */ true); + if (windowInfo.insetsState != null) { + putInsetsState(windowInfo, windowInfo.insetsState); + } + } + + private void putInsets(WindowInfo info, Rect outInsets, boolean includeNotVisible) { + outInsets.set(0, 0, 0, 0); + for (SystemBar bar : systemsBars) { + if (includeNotVisible || bar.isVisible()) { + bar.putInsets(info.displayFrame, info.frame, outInsets); + } + } + } + + private void putInsetsState(WindowInfo info, InsetsState outInsetsState) { + outInsetsState.setDisplayFrame(info.frame); + ShadowInsetsState outShadowInsetsState = Shadow.extract(outInsetsState); + for (SystemBar bar : systemsBars) { + Shadow.extract(outShadowInsetsState.getOrCreateSource(bar.getId())) + .setFrame(bar.inFrame(info.displayFrame, info.frame)) + .setVisible(bar.isVisible()); + } + } + + private static int dpToPx(int px, int displayId) { + return dpToPx(px, DisplayManagerGlobal.getInstance().getDisplayInfo(displayId)); + } + + private static int dpToPx(int px, DisplayInfo displayInfo) { + float density = displayInfo.logicalDensityDpi / (float) DisplayMetrics.DENSITY_DEFAULT; + return round(density * px); + } + + /** + * Base interface for behavior for a system bar such as status bar or navigation bar. See the + * specific interfaces {@link StatusBarBehavior} and {@link NavigationBarBehavior}. + */ + public interface SystemBarBehavior { + /** + * Returns which side of the screen this system bar should be attached to when rendered on the + * given display ID. The implementation may look up the size of the display to determine the + * side. + */ + Side calculateSide(int displayId); + + /** + * Returns the size of the this system bar when rendered on the given display ID. This is either + * the height or the width based on the return value from {@link #calculateSide(int)}. The + * implementation may look up the size of the display to determine the side. + */ + int calculateSize(int displayId); + } + + /** + * Interface for status bar behavior. See {@link #STANDARD_STATUS_BAR} and {@link #NO_STATUS_BAR} + * for default implementations. Custom status bar behavior can be provided by implementing this + * interface and calling {@link SystemUi#setStatusBarBehavior(StatusBarBehavior)}. + */ + public interface StatusBarBehavior extends SystemBarBehavior {} + + /** + * Interface for navigation bar behavior. See {@link #GESTURAL_NAVIGATION}, {@link + * #THREE_BUTTON_NAVIGATION}, and {@link #NO_NAVIGATION_BAR} for default implementations. Custom + * status bar behavior can be provided by implementing this interface and calling {@link + * SystemUi#setNavigationBarBehavior(NavigationBarBehavior)}. + */ + public interface NavigationBarBehavior extends SystemBarBehavior {} + + /** Base class for a system bar. See {@link StatusBar} and {@link NavigationBar}. */ + public abstract static class SystemBar { + /** Side of the screen a system bar is attached to. */ + public enum Side { + LEFT, + TOP, + RIGHT, + BOTTOM + } + + SystemBar() {} + + abstract int getId(); + + /** Returns which side of the screen this bar is attached to. */ + public abstract Side getSide(); + + /** + * Returns the size of this status bar. Depending on which side of the screen the bar is + * attached to this is either the height (for top and bottom) or width (for left or right). + */ + public abstract int getSize(); + + /** + * Returns true if this status bar is currently visible. Note that this is still tracked even if + * the status bar has 0 size. + */ + public abstract boolean isVisible(); + + void insetFrame(Rect outFrame) { + switch (getSide()) { + case LEFT: + outFrame.left += getSize(); + break; + case TOP: + outFrame.top += getSize(); + break; + case RIGHT: + outFrame.right -= getSize(); + break; + case BOTTOM: + outFrame.bottom -= getSize(); + break; + } + } + + Rect inFrame(Rect displayFrame, Rect frame) { + switch (getSide()) { + case LEFT: + return new Rect(0, 0, max(0, getSize() - frame.left), frame.bottom); + case TOP: + return new Rect(0, 0, frame.right, max(0, getSize() - frame.top)); + case RIGHT: + int rightSize = max(0, getSize() - (displayFrame.right - frame.right)); + return new Rect(frame.right - rightSize, 0, frame.right, frame.bottom); + case BOTTOM: + int bottomSize = max(0, getSize() - (displayFrame.bottom - frame.bottom)); + return new Rect(0, frame.bottom - bottomSize, frame.right, frame.bottom); + } + throw new IllegalStateException(); + } + + void putInsets(Rect displayFrame, Rect frame, Rect insets) { + switch (getSide()) { + case LEFT: + insets.left = max(insets.left, getSize() - frame.left); + break; + case TOP: + insets.top = max(insets.top, getSize() - frame.top); + break; + case RIGHT: + insets.right = max(insets.right, getSize() - (displayFrame.right - frame.right)); + break; + case BOTTOM: + insets.bottom = max(insets.bottom, getSize() - (displayFrame.bottom - frame.bottom)); + break; + } + } + } + + /** Represents the system status bar. */ + public static final class StatusBar extends SystemBar { + private final int displayId; + private StatusBarBehavior behavior = NO_STATUS_BAR; + private boolean isVisible = true; + + StatusBar(int displayId) { + this.displayId = displayId; + } + + @Override + int getId() { + return ShadowInsetsState.STATUS_BARS; + } + + StatusBarBehavior getBehavior() { + return behavior; + } + + void setBehavior(StatusBarBehavior behavior) { + this.behavior = behavior; + } + + @Override + public boolean isVisible() { + return isVisible; + } + + boolean setVisible(boolean isVisible) { + boolean didChange = this.isVisible != isVisible; + this.isVisible = isVisible; + return didChange; + } + + @Override + public Side getSide() { + return behavior.calculateSide(displayId); + } + + @Override + public int getSize() { + return behavior.calculateSize(displayId); + } + + @Nonnull + @Override + public String toString() { + return "StatusBar{isVisible=" + isVisible + "}"; + } + } + + static final class NoStatusBarBehavior implements StatusBarBehavior { + @Override + public Side calculateSide(int displayId) { + return Side.TOP; + } + + @Override + public int calculateSize(int displayId) { + return 0; + } + } + + static final class StandardStatusBarBehavior implements StatusBarBehavior { + private static final int HEIGHT_DP = 24; + + @Override + public Side calculateSide(int displayId) { + return Side.TOP; + } + + @Override + public int calculateSize(int displayId) { + return dpToPx(HEIGHT_DP, displayId); + } + } + + /** Represents the system navigation bar. */ + public static final class NavigationBar extends SystemBar { + private final int displayId; + private NavigationBarBehavior behavior = NO_NAVIGATION_BAR; + private boolean isVisible = true; + + NavigationBar(int displayId) { + this.displayId = displayId; + } + + @Override + int getId() { + return ShadowInsetsState.NAVIGATION_BARS; + } + + NavigationBarBehavior getBehavior() { + return behavior; + } + + void setBehavior(NavigationBarBehavior behavior) { + this.behavior = behavior; + } + + @Override + public boolean isVisible() { + return isVisible; + } + + boolean setVisible(boolean isVisible) { + boolean didChange = this.isVisible != isVisible; + this.isVisible = isVisible; + return didChange; + } + + @Override + public Side getSide() { + return behavior.calculateSide(displayId); + } + + @Override + public int getSize() { + return behavior.calculateSize(displayId); + } + + @Nonnull + @Override + public String toString() { + return "NavigationBar{isVisible=" + isVisible + "}"; + } + } + + private static class NoNavigationBarBehavior implements NavigationBarBehavior { + @Override + public Side calculateSide(int displayId) { + return Side.BOTTOM; + } + + @Override + public int calculateSize(int displayId) { + return 0; + } + } + + private static class GesturalNavigationBarBehavior implements NavigationBarBehavior { + private static final int HEIGHT_DP = 24; + + @Override + public Side calculateSide(int displayId) { + return Side.BOTTOM; + } + + @Override + public int calculateSize(int displayId) { + return dpToPx(HEIGHT_DP, displayId); + } + } + + private static class ButtonNavigationBarBehavior implements NavigationBarBehavior { + private static final int BOTTOM_HEIGHT_DP = 48; + private static final int SIDE_HEIGHT_DP = 42; + private static final int LARGE_SCREEN_DP = 600; + private static final int LARGE_SCREEN_HEIGHT_DP = 56; + + @Override + public Side calculateSide(int displayId) { + return calculateSide(DisplayManagerGlobal.getInstance().getDisplayInfo(displayId)); + } + + private Side calculateSide(DisplayInfo info) { + if (isLargeScreen(info)) { + return Side.BOTTOM; + } else { + switch (info.rotation) { + case Surface.ROTATION_90: + return Side.LEFT; + case Surface.ROTATION_180: + return Side.RIGHT; + default: + return Side.BOTTOM; + } + } + } + + @Override + public int calculateSize(int displayId) { + DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); + int sizeDp = + isLargeScreen(displayInfo) + ? LARGE_SCREEN_HEIGHT_DP + : (calculateSide(displayInfo) == Side.BOTTOM ? BOTTOM_HEIGHT_DP : SIDE_HEIGHT_DP); + return dpToPx(sizeDp, displayInfo); + } + + private boolean isLargeScreen(DisplayInfo info) { + return max(info.logicalWidth, info.logicalHeight) >= dpToPx(LARGE_SCREEN_DP, info); + } + } +} From 58b0d1ca1a07ec44c8e1a17c8f025c04711b5501 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Tue, 19 Nov 2024 07:31:30 -0800 Subject: [PATCH 4/9] Remove unused ShadowAccessibilityNodeInfo.areThereUnrecycledNodes Starting in Android U, the recycling mechanism of AccessibilityNodeInfo has been removed. From our investigation, the ShadowAccessibilityNodeInfo.areThereUnrecycledNodes API is unused, and it requires a lot of logic to keep it working. This is part of the initiative to clean up ShadowAccessibilityNodeInfo. PiperOrigin-RevId: 698009478 --- .../ShadowAccessibilityNodeInfoTest.java | 13 --- .../shadows/ShadowAccessibilityNodeInfo.java | 92 ------------------- 2 files changed, 105 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java index e328b8667d2..869e9359d7e 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java @@ -18,7 +18,6 @@ import android.view.accessibility.AccessibilityWindowInfo; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,15 +35,9 @@ public class ShadowAccessibilityNodeInfoTest { @Before public void setUp() { ShadowAccessibilityNodeInfo.resetObtainedInstances(); - assertThat(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(true)).isEqualTo(false); node = AccessibilityNodeInfo.obtain(); } - @Test - public void shouldHaveObtainedNode() { - assertThat(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(false)).isEqualTo(true); - } - @Test public void shouldHaveZeroBounds() { Rect outBounds = new Rect(); @@ -322,10 +315,4 @@ public void obtainWithNode_afterSetSealed() { AccessibilityNodeInfo node2 = AccessibilityNodeInfo.obtain(node); assertThat(node2.isSealed()).isTrue(); } - - @After - public void tearDown() { - ShadowAccessibilityNodeInfo.resetObtainedInstances(); - assertThat(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(true)).isEqualTo(false); - } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java index dd24db51994..af01baebe86 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java @@ -7,17 +7,14 @@ import android.os.Bundle; import android.util.Pair; -import android.util.SparseArray; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -40,12 +37,6 @@ */ @Implements(AccessibilityNodeInfo.class) public class ShadowAccessibilityNodeInfo { - // Map of obtained instances of the class along with stack traces of how they were obtained - private static final Map obtainedInstances = - new HashMap<>(); - - private static final SparseArray orderedInstances = - new SparseArray<>(); private static int sAllocationCount = 0; @@ -136,9 +127,6 @@ protected static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) { if (shadowInfo.mOriginNodeId == 0) { shadowInfo.mOriginNodeId = sAllocationCount; } - StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(newInfo); - obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); - orderedInstances.put(sAllocationCount, wrapper); return newInfo; } @@ -203,44 +191,10 @@ private static void initShadow(AccessibilityNodeInfo obtainedInstance) { if (shadowObtained.mOriginNodeId == 0) { shadowObtained.mOriginNodeId = sAllocationCount; } - StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance); - obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace()); - orderedInstances.put(sAllocationCount, wrapper); } - /** - * Check for leaked objects that were {@code obtain}ed but never {@code recycle}d. - * - * @param printUnrecycledNodesToSystemErr - if true, stack traces of calls to {@code obtain} that - * lack matching calls to {@code recycle} are dumped to System.err. - * @return {@code true} if there are unrecycled nodes - */ - public static boolean areThereUnrecycledNodes(boolean printUnrecycledNodesToSystemErr) { - checkRealAniDisabled(); - if (printUnrecycledNodesToSystemErr) { - for (final StrictEqualityNodeWrapper wrapper : obtainedInstances.keySet()) { - final ShadowAccessibilityNodeInfo shadow = Shadow.extract(wrapper.mInfo); - - System.err.printf( - "Leaked contentDescription = %s. Stack trace:%n", - shadow.realAccessibilityNodeInfo.getContentDescription()); - for (final StackTraceElement stackTraceElement : obtainedInstances.get(wrapper)) { - System.err.println(stackTraceElement.toString()); - } - } - } - - return (obtainedInstances.size() != 0); - } - - /** - * Clear list of obtained instance objects. {@code areThereUnrecycledNodes} will always return - * false if called immediately afterwards. - */ @Resetter public static void resetObtainedInstances() { - obtainedInstances.clear(); - orderedInstances.clear(); queryFromAppProcessWasEnabled = false; } @@ -250,11 +204,6 @@ protected void recycle() { accessibilityNodeInfoReflector.recycle(); return; } - final StrictEqualityNodeWrapper wrapper = - new StrictEqualityNodeWrapper(realAccessibilityNodeInfo); - if (!obtainedInstances.containsKey(wrapper)) { - throw new IllegalStateException(); - } if (labelFor != null) { labelFor.recycle(); @@ -272,17 +221,6 @@ protected void recycle() { traversalBefore.recycle(); } } - - obtainedInstances.remove(wrapper); - int keyOfWrapper = -1; - for (int i = 0; i < orderedInstances.size(); i++) { - int key = orderedInstances.keyAt(i); - if (orderedInstances.get(key).equals(wrapper)) { - keyOfWrapper = key; - break; - } - } - orderedInstances.remove(keyOfWrapper); } @Implementation @@ -649,36 +587,6 @@ public List> getPerformedActionsWithArgs() { return Collections.unmodifiableList(performedActionAndArgsList); } - /** - * Private class to keep different nodes referring to the same view straight in the - * mObtainedInstances map. - */ - private static class StrictEqualityNodeWrapper { - public final AccessibilityNodeInfo mInfo; - - public StrictEqualityNodeWrapper(AccessibilityNodeInfo info) { - mInfo = info; - } - - @Override - @SuppressWarnings("ReferenceEquality") - public boolean equals(Object object) { - if (object == null) { - return false; - } - if (!(object instanceof StrictEqualityNodeWrapper)) { - return false; - } - final StrictEqualityNodeWrapper wrapper = (StrictEqualityNodeWrapper) object; - return mInfo == wrapper.mInfo; - } - - @Override - public int hashCode() { - return mInfo.hashCode(); - } - } - /** * After {@link AccessibilityNodeInfo#setQueryFromAppProcessEnabled(View, boolean)} is called, we * will have direct access to the real {@link AccessibilityNodeInfo} hierarchy, so we want all From b98afc691201f9860b6a9b40e6557a4e9c1e66a9 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Wed, 20 Nov 2024 09:20:35 -0800 Subject: [PATCH 5/9] Replace use of ReflectionHelpers with reflector in ShadowPausedMessageQueue. ShadowPausedMessageQueue previously used two different reflection mechanisms. This commit converts usage of ReflectionHelpers to reflector. PiperOrigin-RevId: 698415753 --- .../shadows/ShadowPausedMessageQueue.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index d86f6aefaad..1390d0acc51 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -4,8 +4,6 @@ import static android.os.Build.VERSION_CODES.M; import static com.google.common.base.Preconditions.checkState; import static org.robolectric.RuntimeEnvironment.getApiLevel; -import static org.robolectric.shadow.api.Shadow.invokeConstructor; -import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; import android.os.Looper; @@ -25,7 +23,6 @@ import org.robolectric.res.android.NativeObjRegistry; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowMessage.MessageReflector; -import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.Scheduler; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; @@ -54,7 +51,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue { // versions @Implementation protected void __constructor__(boolean quitAllowed) { - invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed)); + reflector(MessageQueueReflector.class, realQueue).__constructor__(quitAllowed); long ptr = nativeQueueRegistry.register(this); reflector(MessageQueueReflector.class, realQueue).setPtr(ptr); clockListener = @@ -123,13 +120,13 @@ void poll(long timeout) { // mark the queue as blocked and wait on a new message. synchronized (realQueue) { if (isIdle()) { - ReflectionHelpers.setField(realQueue, "mBlocked", true); + reflector(MessageQueueReflector.class, realQueue).setBlocked(true); try { realQueue.wait(timeout); } catch (InterruptedException ignored) { // Fall through and unblock with no messages. } finally { - ReflectionHelpers.setField(realQueue, "mBlocked", false); + reflector(MessageQueueReflector.class, realQueue).setBlocked(false); } } } @@ -437,6 +434,9 @@ void drainQueue(Predicate msgProcessor) { /** Accessor interface for {@link MessageQueue}'s internals. */ @ForType(MessageQueue.class) private interface MessageQueueReflector { + @Direct + void __constructor__(boolean quitAllowed); + @Direct boolean enqueueMessage(Message msg, long when); @@ -477,5 +477,8 @@ private interface MessageQueueReflector { @Accessor("mAsyncMessageCount") void setAsyncMessageCount(int asyncMessageCount); + + @Accessor("mBlocked") + void setBlocked(boolean blocked); } } From db4acad5df3cbaf161f50137fea2ca88144e44dc Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 20 Nov 2024 09:29:16 -0800 Subject: [PATCH 6/9] Adjust to method signature changes in latest indevelopment SDK. This commit adjusts shadows and reflection logic to adopt to changes in the 'Baklava' SDK. - a new boolean parameter was added to openCameraDeviceUserAsync and CameraDeviceImpl constructor - a new parameter was added to WifiUsabilityStatsEntry constructor PiperOrigin-RevId: 698418305 --- .../shadows/ShadowCameraDeviceImpl.java | 53 ++++++++++++++++++- .../shadows/ShadowCameraManager.java | 36 +++++++++++-- .../WifiUsabilityStatsEntryBuilder.java | 1 + 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraDeviceImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraDeviceImpl.java index aafc57b259e..9ccf296d716 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraDeviceImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraDeviceImpl.java @@ -23,7 +23,6 @@ import org.robolectric.annotation.ClassName; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; @@ -31,6 +30,7 @@ import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.WithType; +import org.robolectric.versioning.AndroidVersions.Baklava; import org.robolectric.versioning.AndroidVersions.V; /** Shadow class for {@link CameraDeviceImpl} */ @@ -40,7 +40,6 @@ public class ShadowCameraDeviceImpl { private boolean closed = false; @Implementation(minSdk = V.SDK_INT) - @InDevelopment protected void __constructor__( String cameraId, StateCallback callback, @@ -75,6 +74,43 @@ protected void __constructor__( .setDeviceExecutor(MoreExecutors.directExecutor()); } + @Implementation(minSdk = Baklava.SDK_INT) + protected void __constructor__( + String cameraId, + StateCallback callback, + Executor executor, + CameraCharacteristics characteristics, + CameraManager cameraManager, + int appTargetSdkVersion, + Context ctx, + @ClassName("android.hardware.camera2.CameraDevice$CameraDeviceSetup") + Object cameraDeviceSetup, + boolean unused) { + try { + reflector(CameraDeviceImplReflector.class, realObject) + .__constructor__( + cameraId, + callback, + executor, + characteristics, + cameraManager, + appTargetSdkVersion, + ctx, + // TODO(juliansull) Remove once Robolectric compiles against Android V + Class.forName("android.hardware.camera2.CameraDevice$CameraDeviceSetup") + .cast(cameraDeviceSetup), + unused); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + // When singleThreadedDeviceExecutor flag is set, this gets put on a background thread. + // This isn't necessary for Robolectric as there is no real camera, so we default back to the + // given executor. + reflector(CameraDeviceImplReflector.class, realObject) + .setDeviceExecutor(MoreExecutors.directExecutor()); + } + @Implementation protected CaptureRequest.Builder createCaptureRequest(int templateType) { checkIfCameraClosedOrInError(); @@ -167,6 +203,19 @@ void __constructor__( @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup); + @Direct + void __constructor__( + String cameraId, + StateCallback callback, + Executor executor, + CameraCharacteristics characteristics, + CameraManager cameraManager, + int appTargetSdkVersion, + Context ctx, + @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") + Object cameraDeviceSetup, + boolean unused); + @Accessor("mDeviceExecutor") void setDeviceExecutor(Executor executor); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java index e4191fecca3..3db8c80d839 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java @@ -27,7 +27,6 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; import org.robolectric.util.ReflectionHelpers; @@ -36,6 +35,7 @@ import org.robolectric.util.reflector.Constructor; import org.robolectric.util.reflector.ForType; import org.robolectric.util.reflector.WithType; +import org.robolectric.versioning.AndroidVersions.Baklava; import org.robolectric.versioning.AndroidVersions.U; import org.robolectric.versioning.AndroidVersions.V; @@ -138,14 +138,14 @@ protected CameraDevice openCameraDeviceUserAsync( // in development API has reverted back to the T signature. Just use a different method name // to avoid conflicts. // TODO: increment this to minSdk next-SDK-after-V once V is fully released - @Implementation(methodName = "openCameraDeviceUserAsync", minSdk = V.SDK_INT) - @InDevelopment + @Implementation(methodName = "openCameraDeviceUserAsync", minSdk = Baklava.SDK_INT) protected CameraDevice openCameraDeviceUserAsyncPostV( String cameraId, CameraDevice.StateCallback callback, Executor executor, int unusedClientUid, - int unusedOomScoreOffset) { + int unusedOomScoreOffset, + boolean unused) { return openCameraDeviceUserAsync( cameraId, callback, executor, unusedClientUid, unusedOomScoreOffset); } @@ -285,7 +285,20 @@ private CameraDeviceImpl createCameraDeviceImpl( CameraCharacteristics characteristics, Context context) { Map cameraCharacteristicsMap = Collections.emptyMap(); - if (RuntimeEnvironment.getApiLevel() >= V.SDK_INT) { + if (RuntimeEnvironment.getApiLevel() >= Baklava.SDK_INT) { + return reflector(ReflectorCameraDeviceImpl.class) + .newCameraDeviceImplPostV( + cameraId, + callback, + executor, + characteristics, + realObject, + context.getApplicationInfo().targetSdkVersion, + context, + null, + false); + + } else if (RuntimeEnvironment.getApiLevel() == V.SDK_INT) { return reflector(ReflectorCameraDeviceImpl.class) .newCameraDeviceImplV( cameraId, @@ -414,6 +427,19 @@ CameraDeviceImpl newCameraDeviceImplV( Context context, @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup); + + @Constructor + CameraDeviceImpl newCameraDeviceImplPostV( + String cameraId, + CameraDevice.StateCallback callback, + Executor executor, + CameraCharacteristics characteristics, + CameraManager cameraManager, + int targetSdkVersion, + Context context, + @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") + Object cameraDeviceSetup, + boolean unused); } /** Accessor interface for {@link CameraManager}'s internals. */ diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java index 9333de87efa..0df6d0f87db 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java @@ -133,6 +133,7 @@ public WifiUsabilityStatsEntry build() { ClassParameter.from(boolean.class, false), ClassParameter.from(int.class, 0), ClassParameter.from(int.class, 0), + ClassParameter.from(int.class, 0), ClassParameter.from(int.class, 0) /* end new in post V */ ); From 87450adceccccede1ce2748dcfddc430096c4219 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Wed, 20 Nov 2024 11:49:31 -0800 Subject: [PATCH 7/9] Update minSdk for shadow methods from V to Baklava. Several InDevelopment shadow methods are marked minSdk V, that are not present in the V release. This commit increments them to Baklava. PiperOrigin-RevId: 698467636 --- .../java/org/robolectric/shadows/ShadowLegacyTypeface.java | 6 +++--- .../robolectric/shadows/ShadowNativeAllocationRegistry.java | 4 ++-- .../shadows/ShadowNoopNativeAllocationRegistry.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java index 9a76f478708..a0ccefb0ca4 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java @@ -35,6 +35,7 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import org.robolectric.versioning.AndroidVersions.Baklava; import org.robolectric.versioning.AndroidVersions.T; import org.robolectric.versioning.AndroidVersions.U; import org.robolectric.versioning.AndroidVersions.V; @@ -53,13 +54,12 @@ protected void __constructor__(long fontId) { description = findById(fontId); } - @Implementation(minSdk = U.SDK_INT) - @InDevelopment + @Implementation(minSdk = U.SDK_INT, maxSdk = V.SDK_INT) protected void __constructor__(long fontId, String familyName) { description = findById(fontId); } - @Implementation(minSdk = V.SDK_INT) + @Implementation(minSdk = Baklava.SDK_INT) @InDevelopment protected void __constructor__(long fontId, String familyName, Typeface derivedFrom) { description = findById(fontId); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java index e3a9aaad89e..509b5706684 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java @@ -15,8 +15,8 @@ import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; +import org.robolectric.versioning.AndroidVersions.Baklava; import org.robolectric.versioning.AndroidVersions.U; -import org.robolectric.versioning.AndroidVersions.V; /** Shadow for {@link NativeAllocationRegistry} that is backed by native code */ @Implements( @@ -36,7 +36,7 @@ public class ShadowNativeAllocationRegistry { * behavior of actual class. */ @InDevelopment - @Implementation(minSdk = V.SDK_INT) + @Implementation(minSdk = Baklava.SDK_INT) protected void __constructor__( ClassLoader classLoader, Class clazz, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java index e8019763fd0..8a35a184e2e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java @@ -11,7 +11,7 @@ import org.robolectric.annotation.InDevelopment; import org.robolectric.annotation.RealObject; import org.robolectric.util.ReflectionHelpers.ClassParameter; -import org.robolectric.versioning.AndroidVersions.V; +import org.robolectric.versioning.AndroidVersions.Baklava; /** Shadow for {@link NativeAllocationRegistry} that is a no-op. */ @Implements(value = NativeAllocationRegistry.class, minSdk = N, isInAndroidSdk = false) @@ -38,7 +38,7 @@ protected Runnable registerNativeAllocation(Object referent, long nativePtr) { * behavior of actual class. */ @InDevelopment - @Implementation(minSdk = V.SDK_INT) + @Implementation(minSdk = Baklava.SDK_INT) protected void __constructor__( ClassLoader classLoader, Class clazz, From d8159ae8ac4698bed0db8bb84a03cf3cb47d3523 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Wed, 20 Nov 2024 12:26:05 -0800 Subject: [PATCH 8/9] Remove hashCode and toString from ShadowAccessibilityNodeInfo ShadowAccessibilityNodeInfo was overriding the hashCode and toString methods of AccessibilityNodeInfo. For hashCode, it was overriding it to return zero, which is completely arbitrary and deviates from real Android framework behavior. Remove these shadow methods so the real framework code can be used instead. PiperOrigin-RevId: 698480092 --- .../shadows/ShadowAccessibilityNodeInfo.java | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java index af01baebe86..6ef7ccc515b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java @@ -486,6 +486,7 @@ protected boolean performAction(int action, Bundle arguments) { * Equality check based on reference equality of the Views from which these instances were * created, or the equality of their assigned IDs. */ + @SuppressWarnings("EqualsHashCode") @Implementation @Override public boolean equals(Object object) { @@ -508,19 +509,6 @@ public boolean equals(Object object) { throw new IllegalStateException("Node has neither an ID nor View"); } - @Implementation - @Override - public int hashCode() { - if (useRealAni()) { - return accessibilityNodeInfoReflector.hashCode(); - } - // This is 0 for a reason. If you change it, you will break the obtained - // instances map in a manner that is remarkably difficult to debug. - // Having a dynamic hash code keeps this object from being located - // in the map if it was mutated after being obtained. - return 0; - } - /** * Add a child node to this one. Also initializes the parent field of the child. * @@ -614,21 +602,6 @@ public interface OnPerformActionListener { boolean onPerformAccessibilityAction(int action, Bundle arguments); } - @Override - @Implementation - public String toString() { - if (useRealAni()) { - return accessibilityNodeInfoReflector.toString(); - } - return "ShadowAccessibilityNodeInfo@" - + System.identityHashCode(this) - + ":{text:" - + text - + ", className:" - + realAccessibilityNodeInfo.getClassName() - + "}"; - } - @ForType(AccessibilityNodeInfo.class) interface AccessibilityNodeInfoReflector { @Direct From 079f3583a95bc66a146cb2266c00c447046df740 Mon Sep 17 00:00:00 2001 From: Michael Hoisie Date: Thu, 21 Nov 2024 14:44:53 -0800 Subject: [PATCH 9/9] Use the real WakeLock constructor in PowerManager.newWakeLock This lets the underlying fields of WakeLock get populated and validation of the parameters to occur. Robolectric was allowing invalid WakeLock levels to be used. Fixes #9566 PiperOrigin-RevId: 698921499 --- .../shadows/ShadowPowerManagerTest.java | 38 +++++++++---------- .../shadows/ShadowPowerManager.java | 26 +++++++------ 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java index 4a2c554ef52..1b838672aa2 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPowerManagerTest.java @@ -55,7 +55,7 @@ public void before() { @Test public void acquire_shouldAcquireAndReleaseReferenceCountedLock() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); assertThat(lock.isHeld()).isFalse(); lock.acquire(); assertThat(lock.isHeld()).isTrue(); @@ -87,7 +87,7 @@ public void acquire_shouldLogLatestWakeLock() { ShadowPowerManager.reset(); assertThat(ShadowPowerManager.getLatestWakeLock()).isNull(); - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); lock.acquire(); assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull(); @@ -106,18 +106,18 @@ public void acquire_shouldLogLatestWakeLock() { @Test public void newWakeLock_shouldCreateWakeLock() { - assertThat(powerManager.newWakeLock(0, "TAG")).isNotNull(); + assertThat(powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG")).isNotNull(); } @Test public void newWakeLock_shouldSetWakeLockTag() { - PowerManager.WakeLock wakeLock = powerManager.newWakeLock(0, "FOO"); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "FOO"); assertThat(shadowOf(wakeLock).getTag()).isEqualTo("FOO"); } @Test public void newWakeLock_shouldAcquireAndReleaseNonReferenceCountedLock() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); lock.setReferenceCounted(false); assertThat(lock.isHeld()).isFalse(); @@ -133,7 +133,7 @@ public void newWakeLock_shouldAcquireAndReleaseNonReferenceCountedLock() { @Test public void newWakeLock_shouldThrowRuntimeExceptionIfLockIsUnderlocked() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); try { lock.release(); fail(); @@ -155,7 +155,7 @@ public void isScreenOn_shouldGetAndSet() { @Test public void isReferenceCounted_shouldGetAndSet() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); assertThat(shadowOf(lock).isReferenceCounted()).isTrue(); lock.setReferenceCounted(false); assertThat(shadowOf(lock).isReferenceCounted()).isFalse(); @@ -228,7 +228,7 @@ public void addThermalStatusListener() { @Test public void workSource_shouldGetAndSet() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); WorkSource workSource = new WorkSource(); assertThat(shadowOf(lock).getWorkSource()).isNull(); lock.setWorkSource(workSource); @@ -293,7 +293,7 @@ public void reboot_incrementsTimesRebootedAndAppendsRebootReason() { @Test public void acquire_shouldIncreaseTimesHeld() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); assertThat(shadowOf(lock).getTimesHeld()).isEqualTo(0); @@ -306,7 +306,7 @@ public void acquire_shouldIncreaseTimesHeld() { @Test public void release_shouldNotDecreaseTimesHeld() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); lock.acquire(); lock.acquire(); @@ -473,7 +473,7 @@ public void preR_userspaceReboot_shouldReboot() { @Test public void releaseWithFlags() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); lock.acquire(); lock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); @@ -483,7 +483,7 @@ public void releaseWithFlags() { @Test public void release() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TAG"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TAG"); lock.acquire(); lock.release(); @@ -537,7 +537,7 @@ public void getBatteryDischargePrediction_default() { @Test public void isHeld_neverAcquired_returnsFalse() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); lock.setReferenceCounted(false); assertThat(lock.isHeld()).isFalse(); @@ -545,7 +545,7 @@ public void isHeld_neverAcquired_returnsFalse() { @Test public void isHeld_wakeLockTimeout_returnsFalse() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); lock.setReferenceCounted(false); lock.acquire(100); @@ -556,7 +556,7 @@ public void isHeld_wakeLockTimeout_returnsFalse() { @Test public void isHeld_wakeLockJustTimeout_returnsTrue() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); lock.setReferenceCounted(false); lock.acquire(100); @@ -567,7 +567,7 @@ public void isHeld_wakeLockJustTimeout_returnsTrue() { @Test public void isHeld_wakeLockNotTimeout_returnsTrue() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); lock.setReferenceCounted(false); lock.acquire(100); @@ -578,7 +578,7 @@ public void isHeld_wakeLockNotTimeout_returnsTrue() { @Test public void isHeld_unlimitedWakeLockAcquired_returnsTrue() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); lock.setReferenceCounted(false); lock.acquire(); @@ -589,7 +589,7 @@ public void isHeld_unlimitedWakeLockAcquired_returnsTrue() { @Test public void release_isRefCounted_dequeueTheSmallestTimeoutLock() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); // There are 2 wake lock acquires when calling release(). The wake lock with the smallest // timeout timestamp is release first. @@ -603,7 +603,7 @@ public void release_isRefCounted_dequeueTheSmallestTimeoutLock() { @Test public void release_isRefCounted_dequeueTimeoutLockBeforeUnlimited() { - PowerManager.WakeLock lock = powerManager.newWakeLock(0, "TIMEOUT"); + PowerManager.WakeLock lock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TIMEOUT"); // There are 2 wake lock acquires when calling release(). The lock with timeout 100ms will be // released first. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java index 2396fecc58d..bb3f0e5759b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPowerManager.java @@ -2,7 +2,6 @@ import static android.content.Intent.ACTION_SCREEN_OFF; import static android.content.Intent.ACTION_SCREEN_ON; -import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.O; @@ -50,6 +49,7 @@ import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.reflector.Accessor; +import org.robolectric.util.reflector.Direct; import org.robolectric.util.reflector.ForType; /** Shadow of PowerManager */ @@ -91,8 +91,8 @@ public class ShadowPowerManager { @Implementation protected PowerManager.WakeLock newWakeLock(int flags, String tag) { - PowerManager.WakeLock wl = Shadow.newInstanceOf(PowerManager.WakeLock.class); - ((ShadowWakeLock) Shadow.extract(wl)).setTag(tag); + PowerManager.WakeLock wl = + reflector(PowerManagerReflector.class, realPowerManager).newWakeLock(flags, tag); latestWakeLock = wl; return wl; } @@ -462,10 +462,11 @@ public boolean getAdaptivePowerSaveEnabled() { @Implements(PowerManager.WakeLock.class) public static class ShadowWakeLock { + @RealObject private PowerManager.WakeLock realWakeLock; + private boolean refCounted = true; private WorkSource workSource = null; private int timesHeld = 0; - private String tag = null; private List> timeoutTimestampList = new ArrayList<>(); private void acquireInternal(Optional timeoutOptional) { @@ -570,18 +571,18 @@ public int getTimesHeld() { @HiddenApi @Implementation(minSdk = O) public String getTag() { - return tag; + return reflector(WakeLockReflector.class, realWakeLock).getTag(); } - /** Sets the tag. */ - @Implementation(minSdk = LOLLIPOP_MR1) - protected void setTag(String tag) { - this.tag = tag; + @ForType(PowerManager.WakeLock.class) + private interface WakeLockReflector { + @Accessor("mTag") + String getTag(); } } private Context getContext() { - return reflector(ReflectorPowerManager.class, realPowerManager).getContext(); + return reflector(PowerManagerReflector.class, realPowerManager).getContext(); } @Implementation(minSdk = TIRAMISU) @@ -679,9 +680,12 @@ public List getPorts() { /** Reflector interface for {@link PowerManager}'s internals. */ @ForType(PowerManager.class) - private interface ReflectorPowerManager { + private interface PowerManagerReflector { @Accessor("mContext") Context getContext(); + + @Direct + WakeLock newWakeLock(int flags, String tag); } }