• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.plugin.platform;
6 
7 import static android.view.MotionEvent.PointerCoords;
8 import static android.view.MotionEvent.PointerProperties;
9 
10 import android.annotation.TargetApi;
11 import android.content.Context;
12 import android.os.Build;
13 import android.support.annotation.UiThread;
14 import android.util.DisplayMetrics;
15 import android.support.annotation.NonNull;
16 import android.util.Log;
17 import android.view.MotionEvent;
18 import android.view.View;
19 
20 import io.flutter.embedding.engine.dart.DartExecutor;
21 import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
22 import io.flutter.plugin.editing.TextInputPlugin;
23 import io.flutter.view.AccessibilityBridge;
24 import io.flutter.view.TextureRegistry;
25 
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 
30 /**
31  * Manages platform views.
32  * <p>
33  * Each {@link io.flutter.app.FlutterPluginRegistry} has a single platform views controller.
34  * A platform views controller can be attached to at most one Flutter view.
35  */
36 public class PlatformViewsController implements PlatformViewsAccessibilityDelegate {
37     private static final String TAG = "PlatformViewsController";
38 
39     // API level 20 is required for VirtualDisplay#setSurface which we use when resizing a platform view.
40     private static final int MINIMAL_SDK = Build.VERSION_CODES.KITKAT_WATCH;
41 
42     private final PlatformViewRegistryImpl registry;
43 
44     // The context of the Activity or Fragment hosting the render target for the Flutter engine.
45     private Context context;
46 
47     // The texture registry maintaining the textures into which the embedded views will be rendered.
48     private TextureRegistry textureRegistry;
49 
50     private TextInputPlugin textInputPlugin;
51 
52     // The system channel used to communicate with the framework about platform views.
53     private PlatformViewsChannel platformViewsChannel;
54 
55     // The accessibility bridge to which accessibility events form the platform views will be dispatched.
56     private final AccessibilityEventsDelegate accessibilityEventsDelegate;
57 
58     private final HashMap<Integer, VirtualDisplayController> vdControllers;
59 
60     // Maps a virtual display's context to the platform view hosted in this virtual display.
61     // Since each virtual display has it's unique context this allows associating any view with the platform view that
62     // it is associated with(e.g if a platform view creates other views in the same virtual display.
63     private final HashMap<Context, View> contextToPlatformView;
64 
65     private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() {
66         @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
67         @Override
68         public long createPlatformView(@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
69             ensureValidAndroidVersion();
70 
71             if (!validateDirection(request.direction)) {
72                 throw new IllegalStateException("Trying to create a view with unknown direction value: "
73                     + request.direction + "(view id: " + request.viewId + ")");
74             }
75 
76             if (vdControllers.containsKey(request.viewId)) {
77                 throw new IllegalStateException("Trying to create an already created platform view, view id: "
78                     + request.viewId);
79             }
80 
81             PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
82             if (viewFactory == null) {
83                 throw new IllegalStateException("Trying to create a platform view of unregistered type: "
84                     + request.viewType);
85             }
86 
87             Object createParams = null;
88             if (request.params != null) {
89                 createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
90             }
91 
92             int physicalWidth = toPhysicalPixels(request.logicalWidth);
93             int physicalHeight = toPhysicalPixels(request.logicalHeight);
94             validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
95 
96             TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
97             VirtualDisplayController vdController = VirtualDisplayController.create(
98                     context,
99                     accessibilityEventsDelegate,
100                     viewFactory,
101                     textureEntry,
102                     physicalWidth,
103                     physicalHeight,
104                     request.viewId,
105                     createParams,
106                     (view, hasFocus) -> {
107                         if (hasFocus) {
108                             platformViewsChannel.invokeViewFocused(request.viewId);
109                         }
110                     }
111             );
112 
113             if (vdController == null) {
114                 throw new IllegalStateException("Failed creating virtual display for a "
115                     + request.viewType + " with id: " + request.viewId);
116             }
117 
118             vdControllers.put(request.viewId, vdController);
119             View platformView = vdController.getView();
120             platformView.setLayoutDirection(request.direction);
121             contextToPlatformView.put(platformView.getContext(), platformView);
122 
123             // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
124 
125             return textureEntry.id();
126         }
127 
128         @Override
129         public void disposePlatformView(int viewId) {
130             ensureValidAndroidVersion();
131 
132             VirtualDisplayController vdController = vdControllers.get(viewId);
133             if (vdController == null) {
134                 throw new IllegalStateException("Trying to dispose a platform view with unknown id: "
135                     + viewId);
136             }
137 
138             if (textInputPlugin != null) {
139                 textInputPlugin.clearPlatformViewClient(viewId);
140             }
141 
142             contextToPlatformView.remove(vdController.getView().getContext());
143             vdController.dispose();
144             vdControllers.remove(viewId);
145         }
146 
147         @Override
148         public void resizePlatformView(@NonNull PlatformViewsChannel.PlatformViewResizeRequest request, @NonNull Runnable onComplete) {
149             ensureValidAndroidVersion();
150 
151             final VirtualDisplayController vdController = vdControllers.get(request.viewId);
152             if (vdController == null) {
153                 throw new IllegalStateException("Trying to resize a platform view with unknown id: "
154                     + request.viewId);
155             }
156 
157             int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
158             int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
159             validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
160 
161             // Resizing involved moving the platform view to a new virtual display. Doing so
162             // potentially results in losing an active input connection. To make sure we preserve
163             // the input connection when resizing we lock it here and unlock after the resize is
164             // complete.
165             lockInputConnection(vdController);
166             vdController.resize(
167                     physicalWidth,
168                     physicalHeight,
169                     new Runnable() {
170                         @Override
171                         public void run() {
172                             unlockInputConnection(vdController);
173                             onComplete.run();
174                         }
175                     }
176             );
177         }
178 
179         @Override
180         public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
181             ensureValidAndroidVersion();
182 
183             float density = context.getResources().getDisplayMetrics().density;
184             PointerProperties[] pointerProperties =
185                 parsePointerPropertiesList(touch.rawPointerPropertiesList)
186                     .toArray(new PointerProperties[touch.pointerCount]);
187             PointerCoords[] pointerCoords =
188                 parsePointerCoordsList(touch.rawPointerCoords, density)
189                     .toArray(new PointerCoords[touch.pointerCount]);
190 
191             if (!vdControllers.containsKey(touch.viewId)) {
192                 throw new IllegalStateException("Sending touch to an unknown view with id: " + touch.viewId);
193             }
194             View view = vdControllers.get(touch.viewId).getView();
195 
196             MotionEvent event = MotionEvent.obtain(
197                 touch.downTime.longValue(),
198                 touch.eventTime.longValue(),
199                 touch.action,
200                 touch.pointerCount,
201                 pointerProperties,
202                 pointerCoords,
203                 touch.metaState,
204                 touch.buttonState,
205                 touch.xPrecision,
206                 touch.yPrecision,
207                 touch.deviceId,
208                 touch.edgeFlags,
209                 touch.source,
210                 touch.flags
211             );
212 
213             view.dispatchTouchEvent(event);
214         }
215 
216         @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
217         @Override
218         public void setDirection(int viewId, int direction) {
219             ensureValidAndroidVersion();
220 
221             if (!validateDirection(direction)) {
222                 throw new IllegalStateException("Trying to set unknown direction value: " + direction
223                     + "(view id: " + viewId + ")");
224             }
225 
226             View view = vdControllers.get(viewId).getView();
227             if (view == null) {
228                 throw new IllegalStateException("Sending touch to an unknown view with id: "
229                     + direction);
230             }
231 
232             view.setLayoutDirection(direction);
233         }
234 
235         @Override
236         public void clearFocus(int viewId) {
237             View view = vdControllers.get(viewId).getView();
238             view.clearFocus();
239         }
240 
241         private void ensureValidAndroidVersion() {
242             if (Build.VERSION.SDK_INT < MINIMAL_SDK) {
243                 Log.e(TAG, "Trying to use platform views with API " + Build.VERSION.SDK_INT
244                     + ", required API level is: " + MINIMAL_SDK);
245                 throw new IllegalStateException("An attempt was made to use platform views on a"
246                     + " version of Android that platform views does not support.");
247             }
248         }
249     };
250 
PlatformViewsController()251     public PlatformViewsController() {
252         registry = new PlatformViewRegistryImpl();
253         vdControllers = new HashMap<>();
254         accessibilityEventsDelegate = new AccessibilityEventsDelegate();
255         contextToPlatformView = new HashMap<>();
256     }
257 
258     /**
259      * Attaches this platform views controller to its input and output channels.
260      *
261      * @param context The base context that will be passed to embedded views created by this controller.
262      *                This should be the context of the Activity hosting the Flutter application.
263      * @param textureRegistry The texture registry which provides the output textures into which the embedded views
264      *                        will be rendered.
265      * @param dartExecutor The dart execution context, which is used to setup a system channel.
266      */
attach(Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor)267     public void attach(Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor) {
268         if (this.context != null) {
269             throw new AssertionError(
270                     "A PlatformViewsController can only be attached to a single output target.\n" +
271                             "attach was called while the PlatformViewsController was already attached."
272             );
273         }
274         this.context = context;
275         this.textureRegistry = textureRegistry;
276         platformViewsChannel = new PlatformViewsChannel(dartExecutor);
277         platformViewsChannel.setPlatformViewsHandler(channelHandler);
278     }
279 
280     /**
281      * Detaches this platform views controller.
282      *
283      * This is typically called when a Flutter applications moves to run in the background, or is destroyed.
284      * After calling this the platform views controller will no longer listen to it's previous messenger, and will
285      * not maintain references to the texture registry, context, and messenger passed to the previous attach call.
286      */
287     @UiThread
detach()288     public void detach() {
289         platformViewsChannel.setPlatformViewsHandler(null);
290         platformViewsChannel = null;
291         context = null;
292         textureRegistry = null;
293     }
294 
295     @Override
attachAccessibilityBridge(AccessibilityBridge accessibilityBridge)296     public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) {
297         accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
298     }
299 
300     @Override
detachAccessibiltyBridge()301     public void detachAccessibiltyBridge() {
302         accessibilityEventsDelegate.setAccessibilityBridge(null);
303     }
304 
305     /**
306      * Attaches this controller to a text input plugin.
307      *
308      * While a text input plugin is available, the platform views controller interacts with it to facilitate
309      * delegation of text input connections to platform views.
310      *
311      * A platform views controller should be attached to a text input plugin whenever it is possible for the Flutter
312      * framework to receive text input.
313      */
attachTextInputPlugin(TextInputPlugin textInputPlugin)314     public void attachTextInputPlugin(TextInputPlugin textInputPlugin) {
315         this.textInputPlugin = textInputPlugin;
316     }
317 
318     /**
319      * Detaches this controller from the currently attached text input plugin.
320      */
detachTextInputPlugin()321     public void detachTextInputPlugin() {
322         textInputPlugin = null;
323     }
324 
325     /**
326      * Returns true if Flutter should perform input connection proxying for the view.
327      *
328      * If the view is a platform view managed by this platform views controller returns true.
329      * Else if the view was created in a platform view's VD, delegates the decision to the platform view's
330      * {@link View#checkInputConnectionProxy(View)} method.
331      * Else returns false.
332      */
checkInputConnectionProxy(View view)333     public boolean checkInputConnectionProxy(View view) {
334         if(!contextToPlatformView.containsKey(view.getContext())) {
335             return false;
336         }
337         View platformView = contextToPlatformView.get(view.getContext());
338         if (platformView == view) {
339             return true;
340         }
341         return platformView.checkInputConnectionProxy(view);
342     }
343 
getRegistry()344     public PlatformViewRegistry getRegistry() {
345         return registry;
346     }
347 
onFlutterViewDestroyed()348     public void onFlutterViewDestroyed() {
349         flushAllViews();
350     }
351 
onPreEngineRestart()352     public void onPreEngineRestart() {
353         flushAllViews();
354     }
355 
356     @Override
getPlatformViewById(Integer id)357     public View getPlatformViewById(Integer id) {
358         VirtualDisplayController controller = vdControllers.get(id);
359         if (controller == null) {
360             return null;
361         }
362         return controller.getView();
363     }
364 
lockInputConnection(@onNull VirtualDisplayController controller)365     private void lockInputConnection(@NonNull VirtualDisplayController controller) {
366         if (textInputPlugin == null) {
367             return;
368         }
369         textInputPlugin.lockPlatformViewInputConnection();
370         controller.onInputConnectionLocked();
371     }
372 
unlockInputConnection(@onNull VirtualDisplayController controller)373     private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
374         if (textInputPlugin == null) {
375             return;
376         }
377         textInputPlugin.unlockPlatformViewInputConnection();
378         controller.onInputConnectionUnlocked();
379     }
380 
validateDirection(int direction)381     private static boolean validateDirection(int direction) {
382         return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL;
383     }
384 
385     @SuppressWarnings("unchecked")
parsePointerPropertiesList(Object rawPropertiesList)386     private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) {
387         List<Object> rawProperties = (List<Object>) rawPropertiesList;
388         List<PointerProperties> pointerProperties = new ArrayList<>();
389         for (Object o : rawProperties) {
390             pointerProperties.add(parsePointerProperties(o));
391         }
392         return pointerProperties;
393     }
394 
395     @SuppressWarnings("unchecked")
parsePointerProperties(Object rawProperties)396     private static PointerProperties parsePointerProperties(Object rawProperties) {
397         List<Object> propertiesList = (List<Object>) rawProperties;
398         PointerProperties properties = new MotionEvent.PointerProperties();
399         properties.id = (int) propertiesList.get(0);
400         properties.toolType = (int) propertiesList.get(1);
401         return properties;
402     }
403 
404     @SuppressWarnings("unchecked")
parsePointerCoordsList(Object rawCoordsList, float density)405     private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) {
406         List<Object> rawCoords = (List<Object>) rawCoordsList;
407         List<PointerCoords> pointerCoords = new ArrayList<>();
408         for (Object o : rawCoords) {
409             pointerCoords.add(parsePointerCoords(o, density));
410         }
411         return pointerCoords;
412     }
413 
414     @SuppressWarnings("unchecked")
parsePointerCoords(Object rawCoords, float density)415     private static PointerCoords parsePointerCoords(Object rawCoords, float density) {
416         List<Object> coordsList = (List<Object>) rawCoords;
417         PointerCoords coords = new MotionEvent.PointerCoords();
418         coords.orientation = (float) (double) coordsList.get(0);
419         coords.pressure = (float) (double) coordsList.get(1);
420         coords.size = (float) (double) coordsList.get(2);
421         coords.toolMajor = (float) (double) coordsList.get(3) * density;
422         coords.toolMinor = (float) (double) coordsList.get(4) * density;
423         coords.touchMajor = (float) (double) coordsList.get(5) * density;
424         coords.touchMinor = (float) (double) coordsList.get(6) * density;
425         coords.x = (float) (double) coordsList.get(7) * density;
426         coords.y = (float) (double) coordsList.get(8) * density;
427         return coords;
428     }
429 
430     // Creating a VirtualDisplay larger than the size of the device screen size
431     // could cause the device to restart: https://github.com/flutter/flutter/issues/28978
validateVirtualDisplayDimensions(int width, int height)432     private void validateVirtualDisplayDimensions(int width, int height) {
433         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
434         if (height > metrics.heightPixels || width > metrics.widthPixels) {
435             String message = "Creating a virtual display of size: "
436                 +  "[" + width + ", " + height + "] may result in problems"
437                 +  "(https://github.com/flutter/flutter/issues/2897)."
438                 +  "It is larger than the device screen size: "
439                 +  "[" + metrics.widthPixels + ", " + metrics.heightPixels + "].";
440             Log.w(TAG, message);
441         }
442     }
443 
toPhysicalPixels(double logicalPixels)444     private int toPhysicalPixels(double logicalPixels) {
445         float density = context.getResources().getDisplayMetrics().density;
446         return (int) Math.round(logicalPixels * density);
447     }
448 
flushAllViews()449     private void flushAllViews() {
450         for (VirtualDisplayController controller : vdControllers.values()) {
451             controller.dispose();
452         }
453         vdControllers.clear();
454     }
455 }
456