1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 4 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 5 import static android.os.Build.VERSION_CODES.P; 6 import static android.os.Build.VERSION_CODES.R; 7 import static org.robolectric.shadows.ShadowView.useRealGraphics; 8 import static org.robolectric.util.reflector.Reflector.reflector; 9 10 import android.app.Instrumentation; 11 import android.content.ClipData; 12 import android.content.Context; 13 import android.os.Binder; 14 import android.os.IBinder; 15 import android.os.Looper; 16 import android.os.RemoteException; 17 import android.os.ServiceManager; 18 import android.view.IWindowManager; 19 import android.view.IWindowSession; 20 import android.view.View; 21 import android.view.WindowManagerGlobal; 22 import androidx.annotation.Nullable; 23 import java.lang.reflect.Proxy; 24 import java.util.List; 25 import org.robolectric.RuntimeEnvironment; 26 import org.robolectric.annotation.Implementation; 27 import org.robolectric.annotation.Implements; 28 import org.robolectric.annotation.Resetter; 29 import org.robolectric.util.ReflectionHelpers; 30 import org.robolectric.util.reflector.Accessor; 31 import org.robolectric.util.reflector.ForType; 32 import org.robolectric.util.reflector.Static; 33 34 /** Shadow for {@link WindowManagerGlobal}. */ 35 @SuppressWarnings("unused") // Unused params are implementations of Android SDK methods. 36 @Implements( 37 value = WindowManagerGlobal.class, 38 isInAndroidSdk = false, 39 minSdk = JELLY_BEAN_MR1, 40 looseSignatures = true) 41 public class ShadowWindowManagerGlobal { 42 private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate(); 43 private static IWindowSession windowSession; 44 45 @Resetter reset()46 public static void reset() { 47 reflector(WindowManagerGlobalReflector.class).setDefaultWindowManager(null); 48 windowSessionDelegate = new WindowSessionDelegate(); 49 windowSession = null; 50 } 51 getInTouchMode()52 public static boolean getInTouchMode() { 53 return windowSessionDelegate.getInTouchMode(); 54 } 55 56 /** 57 * Sets whether the window manager is in touch mode. Use {@link 58 * Instrumentation#setInTouchMode(boolean)} to modify this from a test. 59 */ setInTouchMode(boolean inTouchMode)60 static void setInTouchMode(boolean inTouchMode) { 61 windowSessionDelegate.setInTouchMode(inTouchMode); 62 } 63 64 /** 65 * Returns the last {@link ClipData} passed to a drag initiated from a call to {@link 66 * View#startDrag} or {@link View#startDragAndDrop}, or null if there isn't one. 67 */ 68 @Nullable getLastDragClipData()69 public static ClipData getLastDragClipData() { 70 return windowSessionDelegate.lastDragClipData; 71 } 72 73 /** Clears the data returned by {@link #getLastDragClipData()}. */ clearLastDragClipData()74 public static void clearLastDragClipData() { 75 windowSessionDelegate.lastDragClipData = null; 76 } 77 78 @Implementation(minSdk = JELLY_BEAN_MR2) getWindowSession()79 protected static synchronized IWindowSession getWindowSession() { 80 if (windowSession == null) { 81 // Use Proxy.newProxyInstance instead of ReflectionHelpers.createDelegatingProxy as there are 82 // too many variants of 'add', 'addToDisplay', and 'addToDisplayAsUser', some of which have 83 // arg types that don't exist any more. 84 windowSession = 85 (IWindowSession) 86 Proxy.newProxyInstance( 87 IWindowSession.class.getClassLoader(), 88 new Class<?>[] {IWindowSession.class}, 89 (proxy, method, args) -> { 90 String methodName = method.getName(); 91 switch (methodName) { 92 case "add": // SDK 16 93 case "addToDisplay": // SDK 17-29 94 case "addToDisplayAsUser": // SDK 30+ 95 return windowSessionDelegate.getAddFlags(); 96 case "getInTouchMode": 97 return windowSessionDelegate.getInTouchMode(); 98 case "performDrag": 99 return windowSessionDelegate.performDrag(args); 100 case "prepareDrag": 101 return windowSessionDelegate.prepareDrag(); 102 case "setInTouchMode": 103 windowSessionDelegate.setInTouchMode((boolean) args[0]); 104 return null; 105 default: 106 return ReflectionHelpers.defaultValueForType( 107 method.getReturnType().getName()); 108 } 109 }); 110 } 111 return windowSession; 112 } 113 114 @Implementation(maxSdk = JELLY_BEAN_MR1) getWindowSession(Looper looper)115 protected static Object getWindowSession(Looper looper) { 116 return getWindowSession(); 117 } 118 119 @Implementation peekWindowSession()120 protected static synchronized IWindowSession peekWindowSession() { 121 return windowSession; 122 } 123 124 @Implementation getWindowManagerService()125 public static Object getWindowManagerService() throws RemoteException { 126 IWindowManager service = 127 reflector(WindowManagerGlobalReflector.class).getWindowManagerService(); 128 if (service == null) { 129 service = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); 130 reflector(WindowManagerGlobalReflector.class).setWindowManagerService(service); 131 if (RuntimeEnvironment.getApiLevel() >= R) { 132 reflector(WindowManagerGlobalReflector.class).setUseBlastAdapter(service.useBLAST()); 133 } 134 } 135 return service; 136 } 137 138 @ForType(WindowManagerGlobal.class) 139 interface WindowManagerGlobalReflector { 140 @Accessor("sDefaultWindowManager") 141 @Static setDefaultWindowManager(WindowManagerGlobal global)142 void setDefaultWindowManager(WindowManagerGlobal global); 143 144 @Static 145 @Accessor("sWindowManagerService") getWindowManagerService()146 IWindowManager getWindowManagerService(); 147 148 @Static 149 @Accessor("sWindowManagerService") setWindowManagerService(IWindowManager service)150 void setWindowManagerService(IWindowManager service); 151 152 @Static 153 @Accessor("sUseBLASTAdapter") setUseBlastAdapter(boolean useBlastAdapter)154 void setUseBlastAdapter(boolean useBlastAdapter); 155 156 @Accessor("mViews") getWindowViews()157 List<View> getWindowViews(); 158 } 159 160 private static class WindowSessionDelegate { 161 // From WindowManagerGlobal (was WindowManagerImpl in JB). 162 static final int ADD_FLAG_IN_TOUCH_MODE = 0x1; 163 static final int ADD_FLAG_APP_VISIBLE = 0x2; 164 165 // TODO: Default to touch mode always. 166 private boolean inTouchMode = useRealGraphics(); 167 @Nullable protected ClipData lastDragClipData; 168 getAddFlags()169 protected int getAddFlags() { 170 int res = 0; 171 // Temporarily enable this based on a system property to allow for test migration. This will 172 // eventually be updated to default and true and eventually removed, Robolectric's previous 173 // behavior of not marking windows as visible by default is a bug. This flag should only be 174 // used as a temporary toggle during migration. 175 if (useRealGraphics() 176 || "true".equals(System.getProperty("robolectric.areWindowsMarkedVisible", "false"))) { 177 res |= ADD_FLAG_APP_VISIBLE; 178 } 179 if (getInTouchMode()) { 180 res |= ADD_FLAG_IN_TOUCH_MODE; 181 } 182 return res; 183 } 184 getInTouchMode()185 public boolean getInTouchMode() { 186 return inTouchMode; 187 } 188 setInTouchMode(boolean inTouchMode)189 public void setInTouchMode(boolean inTouchMode) { 190 this.inTouchMode = inTouchMode; 191 } 192 prepareDrag()193 public IBinder prepareDrag() { 194 return new Binder(); 195 } 196 performDrag(Object[] args)197 public Object performDrag(Object[] args) { 198 // extract the clipData param 199 for (int i = args.length - 1; i >= 0; i--) { 200 if (args[i] instanceof ClipData) { 201 lastDragClipData = (ClipData) args[i]; 202 // In P (SDK 28), the return type changed from boolean to Binder. 203 return RuntimeEnvironment.getApiLevel() >= P ? new Binder() : true; 204 } 205 } 206 throw new AssertionError("Missing ClipData param"); 207 } 208 } 209 } 210