• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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