1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.Q; 4 import static android.os.Build.VERSION_CODES.R; 5 import static android.os.Build.VERSION_CODES.S_V2; 6 import static org.robolectric.annotation.TextLayoutMode.Mode.REALISTIC; 7 import static org.robolectric.util.reflector.Reflector.reflector; 8 9 import android.content.res.Configuration; 10 import android.graphics.Rect; 11 import android.os.Build; 12 import android.os.Build.VERSION_CODES; 13 import android.os.RemoteException; 14 import android.util.MergedConfiguration; 15 import android.view.Display; 16 import android.view.HandlerActionQueue; 17 import android.view.InsetsState; 18 import android.view.Surface; 19 import android.view.SurfaceControl; 20 import android.view.View; 21 import android.view.ViewRootImpl; 22 import android.view.WindowInsets; 23 import android.view.WindowManager; 24 import android.window.ClientWindowFrames; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Optional; 28 import org.robolectric.RuntimeEnvironment; 29 import org.robolectric.annotation.Implementation; 30 import org.robolectric.annotation.Implements; 31 import org.robolectric.annotation.RealObject; 32 import org.robolectric.annotation.Resetter; 33 import org.robolectric.annotation.TextLayoutMode; 34 import org.robolectric.config.ConfigurationRegistry; 35 import org.robolectric.shadow.api.Shadow; 36 import org.robolectric.util.ReflectionHelpers; 37 import org.robolectric.util.ReflectionHelpers.ClassParameter; 38 import org.robolectric.util.reflector.Accessor; 39 import org.robolectric.util.reflector.Direct; 40 import org.robolectric.util.reflector.ForType; 41 import org.robolectric.util.reflector.Static; 42 import org.robolectric.util.reflector.WithType; 43 44 @Implements(value = ViewRootImpl.class, isInAndroidSdk = false) 45 public class ShadowViewRootImpl { 46 47 private static final int RELAYOUT_RES_IN_TOUCH_MODE = 0x1; 48 49 @RealObject protected ViewRootImpl realObject; 50 51 /** 52 * The visibility of the system status bar. 53 * 54 * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing 55 * the current state via the returned {@link WindowInsets} instance if it has been set.. 56 * 57 * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the 58 * current window insets. Rather it tracks the latest known state provided via {@link 59 * #setIsStatusBarVisible(boolean)}. 60 */ 61 private static Optional<Boolean> isStatusBarVisible = Optional.empty(); 62 63 /** 64 * The visibility of the system navigation bar. 65 * 66 * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing 67 * the current state via the returned {@link WindowInsets} instance if it has been set. 68 * 69 * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the 70 * current window insets. Rather it tracks the latest known state provided via {@link 71 * #setIsNavigationBarVisible(boolean)}. 72 */ 73 private static Optional<Boolean> isNavigationBarVisible = Optional.empty(); 74 75 /** Allows other shadows to set the state of {@link #isStatusBarVisible}. */ setIsStatusBarVisible(boolean isStatusBarVisible)76 protected static void setIsStatusBarVisible(boolean isStatusBarVisible) { 77 ShadowViewRootImpl.isStatusBarVisible = Optional.of(isStatusBarVisible); 78 } 79 80 /** Clears the last known state of {@link #isStatusBarVisible}. */ clearIsStatusBarVisible()81 protected static void clearIsStatusBarVisible() { 82 ShadowViewRootImpl.isStatusBarVisible = Optional.empty(); 83 } 84 85 /** Allows other shadows to set the state of {@link #isNavigationBarVisible}. */ setIsNavigationBarVisible(boolean isNavigationBarVisible)86 protected static void setIsNavigationBarVisible(boolean isNavigationBarVisible) { 87 ShadowViewRootImpl.isNavigationBarVisible = Optional.of(isNavigationBarVisible); 88 } 89 90 /** Clears the last known state of {@link #isNavigationBarVisible}. */ clearIsNavigationBarVisible()91 protected static void clearIsNavigationBarVisible() { 92 ShadowViewRootImpl.isNavigationBarVisible = Optional.empty(); 93 } 94 95 @Implementation playSoundEffect(int effectId)96 public void playSoundEffect(int effectId) {} 97 98 @Implementation relayoutWindow( WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending)99 protected int relayoutWindow( 100 WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) 101 throws RemoteException { 102 // TODO(christianw): probably should return WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED? 103 int result = 0; 104 if (ShadowWindowManagerGlobal.getInTouchMode() && RuntimeEnvironment.getApiLevel() <= S_V2) { 105 result |= RELAYOUT_RES_IN_TOUCH_MODE; 106 } 107 if (RuntimeEnvironment.getApiLevel() >= Q) { 108 // Simulate initializing the SurfaceControl member object, which happens during this method. 109 SurfaceControl surfaceControl = 110 reflector(ViewRootImplReflector.class, realObject).getSurfaceControl(); 111 ShadowSurfaceControl shadowSurfaceControl = Shadow.extract(surfaceControl); 112 shadowSurfaceControl.initializeNativeObject(); 113 } 114 return result; 115 } 116 callDispatchResized()117 public void callDispatchResized() { 118 Optional<Class<?>> activityWindowInfoClass = 119 ReflectionHelpers.attemptLoadClass( 120 this.getClass().getClassLoader(), "android.window.ActivityWindowInfo"); 121 if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.UPSIDE_DOWN_CAKE 122 && activityWindowInfoClass.isPresent()) { 123 Display display = getDisplay(); 124 Rect frame = new Rect(); 125 display.getRectSize(frame); 126 127 ClientWindowFrames frames = new ClientWindowFrames(); 128 // set the final field 129 ReflectionHelpers.setField(frames, "frame", frame); 130 final ClassParameter<?>[] parameters = 131 new ClassParameter<?>[] { 132 ClassParameter.from(ClientWindowFrames.class, frames), 133 ClassParameter.from(boolean.class, true), /* reportDraw */ 134 ClassParameter.from( 135 MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ 136 ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ 137 ClassParameter.from(boolean.class, false), /* forceLayout */ 138 ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ 139 ClassParameter.from(int.class, 0), /* displayId */ 140 ClassParameter.from(int.class, 0), /* syncSeqId */ 141 ClassParameter.from(boolean.class, false), /* dragResizing */ 142 ClassParameter.from( 143 activityWindowInfoClass.get(), 144 ReflectionHelpers.newInstance( 145 activityWindowInfoClass.get())) /* activityWindowInfo */ 146 }; 147 try { 148 ReflectionHelpers.callInstanceMethod( 149 ViewRootImpl.class, realObject, "dispatchResized", parameters); 150 } catch (RuntimeException ex) { 151 ReflectionHelpers.callInstanceMethod( 152 ViewRootImpl.class, 153 realObject, 154 "dispatchResized", 155 Arrays.copyOfRange(parameters, 0, parameters.length - 1)); 156 } 157 } else if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) { 158 Display display = getDisplay(); 159 Rect frame = new Rect(); 160 display.getRectSize(frame); 161 162 ClientWindowFrames frames = new ClientWindowFrames(); 163 // set the final field 164 ReflectionHelpers.setField(frames, "frame", frame); 165 166 ReflectionHelpers.callInstanceMethod( 167 ViewRootImpl.class, 168 realObject, 169 "dispatchResized", 170 ClassParameter.from(ClientWindowFrames.class, frames), 171 ClassParameter.from(boolean.class, true), /* reportDraw */ 172 ClassParameter.from( 173 MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ 174 ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ 175 ClassParameter.from(boolean.class, false), /* forceLayout */ 176 ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ 177 ClassParameter.from(int.class, 0), /* displayId */ 178 ClassParameter.from(int.class, 0), /* syncSeqId */ 179 ClassParameter.from(boolean.class, false) /* dragResizing */); 180 } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.S_V2) { 181 Display display = getDisplay(); 182 Rect frame = new Rect(); 183 display.getRectSize(frame); 184 185 ClientWindowFrames frames = new ClientWindowFrames(); 186 // set the final field 187 ReflectionHelpers.setField(frames, "frame", frame); 188 189 ReflectionHelpers.callInstanceMethod( 190 ViewRootImpl.class, 191 realObject, 192 "dispatchResized", 193 ClassParameter.from(ClientWindowFrames.class, frames), 194 ClassParameter.from(boolean.class, true), /* reportDraw */ 195 ClassParameter.from( 196 MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ 197 ClassParameter.from(InsetsState.class, new InsetsState()), /* insetsState */ 198 ClassParameter.from(boolean.class, false), /* forceLayout */ 199 ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ 200 ClassParameter.from(int.class, 0), /* displayId */ 201 ClassParameter.from(int.class, 0), /* syncSeqId */ 202 ClassParameter.from(int.class, 0) /* resizeMode */); 203 } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.R) { 204 Display display = getDisplay(); 205 Rect frame = new Rect(); 206 display.getRectSize(frame); 207 208 ClientWindowFrames frames = new ClientWindowFrames(); 209 // set the final field 210 ReflectionHelpers.setField(frames, "frame", frame); 211 212 ReflectionHelpers.callInstanceMethod( 213 ViewRootImpl.class, 214 realObject, 215 "dispatchResized", 216 ClassParameter.from(ClientWindowFrames.class, frames), 217 ClassParameter.from(boolean.class, true), /* reportDraw */ 218 ClassParameter.from( 219 MergedConfiguration.class, new MergedConfiguration()), /* mergedConfiguration */ 220 ClassParameter.from(boolean.class, false), /* forceLayout */ 221 ClassParameter.from(boolean.class, false), /* alwaysConsumeSystemBars */ 222 ClassParameter.from(int.class, 0) /* displayId */); 223 } else if (RuntimeEnvironment.getApiLevel() > Build.VERSION_CODES.Q) { 224 Display display = getDisplay(); 225 Rect frame = new Rect(); 226 display.getRectSize(frame); 227 228 Rect emptyRect = new Rect(0, 0, 0, 0); 229 ReflectionHelpers.callInstanceMethod( 230 ViewRootImpl.class, 231 realObject, 232 "dispatchResized", 233 ClassParameter.from(Rect.class, frame), 234 ClassParameter.from(Rect.class, emptyRect), 235 ClassParameter.from(Rect.class, emptyRect), 236 ClassParameter.from(Rect.class, emptyRect), 237 ClassParameter.from(boolean.class, true), 238 ClassParameter.from(MergedConfiguration.class, new MergedConfiguration()), 239 ClassParameter.from(Rect.class, frame), 240 ClassParameter.from(boolean.class, false), 241 ClassParameter.from(boolean.class, false), 242 ClassParameter.from(int.class, 0), 243 ClassParameter.from( 244 android.view.DisplayCutout.ParcelableWrapper.class, 245 new android.view.DisplayCutout.ParcelableWrapper())); 246 } else { 247 Display display = getDisplay(); 248 Rect frame = new Rect(); 249 display.getRectSize(frame); 250 reflector(ViewRootImplReflector.class, realObject).dispatchResized(frame); 251 } 252 } 253 getDisplay()254 protected Display getDisplay() { 255 return reflector(ViewRootImplReflector.class, realObject).getDisplay(); 256 } 257 258 @Implementation setView(View view, WindowManager.LayoutParams attrs, View panelParentView)259 protected void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { 260 reflector(ViewRootImplReflector.class, realObject).setView(view, attrs, panelParentView); 261 if (ConfigurationRegistry.get(TextLayoutMode.Mode.class) == REALISTIC) { 262 Rect winFrame = new Rect(); 263 getDisplay().getRectSize(winFrame); 264 reflector(ViewRootImplReflector.class, realObject).setWinFrame(winFrame); 265 } 266 } 267 268 @Implementation(minSdk = R) setView( View view, WindowManager.LayoutParams attrs, View panelParentView, int userId)269 protected void setView( 270 View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) { 271 reflector(ViewRootImplReflector.class, realObject) 272 .setView(view, attrs, panelParentView, userId); 273 if (ConfigurationRegistry.get(TextLayoutMode.Mode.class) == REALISTIC) { 274 Rect winFrame = new Rect(); 275 getDisplay().getRectSize(winFrame); 276 reflector(ViewRootImplReflector.class, realObject).setWinFrame(winFrame); 277 } 278 } 279 280 /** 281 * On Android R+ {@link WindowInsets} supports checking visibility of specific inset types. 282 * 283 * <p>For those SDK levels, override the real {@link WindowInsets} with the tracked system bar 284 * visibility status ({@link #isStatusBarVisible}/{@link #isNavigationBarVisible}), if set. 285 * 286 * <p>NOTE: We use state tracking in place of a longer term solution of implementing the insets 287 * calculations and broadcast (via listeners) for now. Once we have insets calculations working we 288 * should remove this mechanism. 289 */ 290 @Implementation(minSdk = R) getWindowInsets(boolean forceConstruct)291 protected WindowInsets getWindowInsets(boolean forceConstruct) { 292 WindowInsets realInsets = 293 reflector(ViewRootImplReflector.class, realObject).getWindowInsets(forceConstruct); 294 295 WindowInsets.Builder overridenInsetsBuilder = new WindowInsets.Builder(realInsets); 296 297 if (isStatusBarVisible.isPresent()) { 298 overridenInsetsBuilder = 299 overridenInsetsBuilder.setVisible( 300 WindowInsets.Type.statusBars(), isStatusBarVisible.get()); 301 } 302 303 if (isNavigationBarVisible.isPresent()) { 304 overridenInsetsBuilder = 305 overridenInsetsBuilder.setVisible( 306 WindowInsets.Type.navigationBars(), isNavigationBarVisible.get()); 307 } 308 309 return overridenInsetsBuilder.build(); 310 } 311 312 @Resetter reset()313 public static void reset() { 314 ViewRootImplReflector viewRootImplStatic = reflector(ViewRootImplReflector.class); 315 viewRootImplStatic.setRunQueues(new ThreadLocal<>()); 316 viewRootImplStatic.setFirstDrawHandlers(new ArrayList<>()); 317 viewRootImplStatic.setFirstDrawComplete(false); 318 viewRootImplStatic.setConfigCallbacks(new ArrayList<>()); 319 320 clearIsStatusBarVisible(); 321 clearIsNavigationBarVisible(); 322 } 323 callWindowFocusChanged(boolean hasFocus)324 public void callWindowFocusChanged(boolean hasFocus) { 325 if (RuntimeEnvironment.getApiLevel() <= S_V2) { 326 reflector(ViewRootImplReflector.class, realObject) 327 .windowFocusChanged(hasFocus, ShadowWindowManagerGlobal.getInTouchMode()); 328 } else { 329 reflector(ViewRootImplReflector.class, realObject).windowFocusChanged(hasFocus); 330 } 331 } 332 getSurface()333 Surface getSurface() { 334 return reflector(ViewRootImplReflector.class, realObject).getSurface(); 335 } 336 337 /** Reflector interface for {@link ViewRootImpl}'s internals. */ 338 @ForType(ViewRootImpl.class) 339 protected interface ViewRootImplReflector { 340 341 @Direct setView(View view, WindowManager.LayoutParams attrs, View panelParentView)342 void setView(View view, WindowManager.LayoutParams attrs, View panelParentView); 343 344 @Direct setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId)345 void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId); 346 347 @Static 348 @Accessor("sRunQueues") setRunQueues(ThreadLocal<HandlerActionQueue> threadLocal)349 void setRunQueues(ThreadLocal<HandlerActionQueue> threadLocal); 350 351 @Static 352 @Accessor("sFirstDrawHandlers") setFirstDrawHandlers(ArrayList<Runnable> handlers)353 void setFirstDrawHandlers(ArrayList<Runnable> handlers); 354 355 @Static 356 @Accessor("sFirstDrawComplete") setFirstDrawComplete(boolean isComplete)357 void setFirstDrawComplete(boolean isComplete); 358 359 @Static 360 @Accessor("sConfigCallbacks") setConfigCallbacks(ArrayList<ViewRootImpl.ConfigChangedCallback> callbacks)361 void setConfigCallbacks(ArrayList<ViewRootImpl.ConfigChangedCallback> callbacks); 362 363 @Accessor("sNewInsetsMode") 364 @Static getNewInsetsMode()365 int getNewInsetsMode(); 366 367 @Accessor("mWinFrame") setWinFrame(Rect winFrame)368 void setWinFrame(Rect winFrame); 369 370 @Accessor("mDisplay") getDisplay()371 Display getDisplay(); 372 373 @Accessor("mSurfaceControl") getSurfaceControl()374 SurfaceControl getSurfaceControl(); 375 376 @Accessor("mSurface") getSurface()377 Surface getSurface(); 378 379 @Accessor("mWindowAttributes") getWindowAttributes()380 WindowManager.LayoutParams getWindowAttributes(); 381 382 // <= LOLLIPOP_MR1 dispatchResized( Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, boolean reportDraw, Configuration newConfig)383 void dispatchResized( 384 Rect frame, 385 Rect overscanInsets, 386 Rect contentInsets, 387 Rect visibleInsets, 388 Rect stableInsets, 389 boolean reportDraw, 390 Configuration newConfig); 391 392 // <= M dispatchResized( Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, Configuration newConfig)393 void dispatchResized( 394 Rect frame, 395 Rect overscanInsets, 396 Rect contentInsets, 397 Rect visibleInsets, 398 Rect stableInsets, 399 Rect outsets, 400 boolean reportDraw, 401 Configuration newConfig); 402 403 // <= N_MR1 dispatchResized( Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, Configuration newConfig, Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar)404 void dispatchResized( 405 Rect frame, 406 Rect overscanInsets, 407 Rect contentInsets, 408 Rect visibleInsets, 409 Rect stableInsets, 410 Rect outsets, 411 boolean reportDraw, 412 Configuration newConfig, 413 Rect backDropFrame, 414 boolean forceLayout, 415 boolean alwaysConsumeNavBar); 416 417 // <= O_MR1 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)418 void dispatchResized( 419 Rect frame, 420 Rect overscanInsets, 421 Rect contentInsets, 422 Rect visibleInsets, 423 Rect stableInsets, 424 Rect outsets, 425 boolean reportDraw, 426 @WithType("android.util.MergedConfiguration") Object mergedConfiguration, 427 Rect backDropFrame, 428 boolean forceLayout, 429 boolean alwaysConsumeNavBar, 430 int displayId); 431 432 // >= P 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)433 void dispatchResized( 434 Rect frame, 435 Rect overscanInsets, 436 Rect contentInsets, 437 Rect visibleInsets, 438 Rect stableInsets, 439 Rect outsets, 440 boolean reportDraw, 441 @WithType("android.util.MergedConfiguration") Object mergedConfiguration, 442 Rect backDropFrame, 443 boolean forceLayout, 444 boolean alwaysConsumeNavBar, 445 int displayId, 446 @WithType("android.view.DisplayCutout$ParcelableWrapper") Object displayCutout); 447 dispatchResized(Rect frame)448 default void dispatchResized(Rect frame) { 449 Rect emptyRect = new Rect(0, 0, 0, 0); 450 451 int apiLevel = RuntimeEnvironment.getApiLevel(); 452 if (apiLevel <= Build.VERSION_CODES.LOLLIPOP_MR1) { 453 dispatchResized(frame, emptyRect, emptyRect, emptyRect, emptyRect, true, null); 454 } else if (apiLevel <= Build.VERSION_CODES.M) { 455 dispatchResized(frame, emptyRect, emptyRect, emptyRect, emptyRect, emptyRect, true, null); 456 } else if (apiLevel <= Build.VERSION_CODES.N_MR1) { 457 dispatchResized( 458 frame, emptyRect, emptyRect, emptyRect, emptyRect, emptyRect, true, null, frame, false, 459 false); 460 } else if (apiLevel <= Build.VERSION_CODES.O_MR1) { 461 dispatchResized( 462 frame, 463 emptyRect, 464 emptyRect, 465 emptyRect, 466 emptyRect, 467 emptyRect, 468 true, 469 new MergedConfiguration(), 470 frame, 471 false, 472 false, 473 0); 474 } else { // apiLevel >= Build.VERSION_CODES.P 475 dispatchResized( 476 frame, 477 emptyRect, 478 emptyRect, 479 emptyRect, 480 emptyRect, 481 emptyRect, 482 true, 483 new MergedConfiguration(), 484 frame, 485 false, 486 false, 487 0, 488 new android.view.DisplayCutout.ParcelableWrapper()); 489 } 490 } 491 492 // SDK <= S_V2 windowFocusChanged(boolean hasFocus, boolean inTouchMode)493 void windowFocusChanged(boolean hasFocus, boolean inTouchMode); 494 495 // SDK >= T windowFocusChanged(boolean hasFocus)496 void windowFocusChanged(boolean hasFocus); 497 498 // SDK >= M 499 @Direct getWindowInsets(boolean forceConstruct)500 WindowInsets getWindowInsets(boolean forceConstruct); 501 } 502 } 503