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