1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget.inline;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.TestApi;
22 import android.content.Context;
23 import android.graphics.PixelFormat;
24 import android.graphics.PointF;
25 import android.graphics.Rect;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.SurfaceControl;
29 import android.view.SurfaceControlViewHost;
30 import android.view.SurfaceHolder;
31 import android.view.SurfaceView;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.ViewTreeObserver;
35 
36 import java.lang.ref.WeakReference;
37 import java.util.function.Consumer;
38 
39 /**
40  * This class represents a view that holds opaque content from another app that you can inline in
41  * your UI.
42  *
43  * <p>Since the content presented by this view is from another security domain,it is
44  * shown on a remote surface preventing the host application from accessing that content. Also the
45  * host application cannot interact with the inlined content by injecting touch events or clicking
46  * programmatically.
47  *
48  * <p>This view can be overlaid by other windows, i.e. redressed, but if this is the case
49  * the inlined UI would not be interactive. Sometimes this is desirable, e.g. animating transitions.
50  *
51  * <p>By default the surface backing this view is shown on top of the hosting window such
52  * that the inlined content is interactive. However, you can temporarily move the surface under the
53  * hosting window which could be useful in some cases, e.g. animating transitions. At this point the
54  * inlined content will not be interactive and the touch events would be delivered to your app.
55  *
56  * <p> Instances of this class are created by the platform and can be programmatically attached to
57  * your UI. Once the view is attached to the window, you may detach and reattach it to the window.
58  * It should work seamlessly from the hosting process's point of view.
59  */
60 public class InlineContentView extends ViewGroup {
61 
62     private static final String TAG = "InlineContentView";
63 
64     private static final boolean DEBUG = false;
65 
66     /**
67      * Callback for observing the lifecycle of the surface control that manipulates the backing
68      * secure embedded UI surface.
69      */
70     public interface SurfaceControlCallback {
71         /**
72          * Called when the backing surface is being created.
73          *
74          * @param surfaceControl The surface control to manipulate the surface.
75          */
onCreated(@onNull SurfaceControl surfaceControl)76         void onCreated(@NonNull SurfaceControl surfaceControl);
77 
78         /**
79          * Called when the backing surface is being destroyed.
80          *
81          * @param surfaceControl The surface control to manipulate the surface.
82          */
onDestroyed(@onNull SurfaceControl surfaceControl)83         void onDestroyed(@NonNull SurfaceControl surfaceControl);
84     }
85 
86     /**
87      * Callback for sending an updated surface package in case the previous one is released
88      * from the detached from window event, and for getting notified of such event.
89      *
90      * This is expected to be provided to the {@link InlineContentView} so it can get updates
91      * from and send updates to the remote content (i.e. surface package) provider.
92      *
93      * @hide
94      */
95     @TestApi
96     public interface SurfacePackageUpdater {
97 
98 
99         /**
100          * Called when the previous surface package is released due to view being detached
101          * from the window.
102          */
onSurfacePackageReleased()103         void onSurfacePackageReleased();
104 
105         /**
106          * Called to request an updated surface package.
107          *
108          * @param consumer consumes the updated surface package.
109          */
getSurfacePackage(@onNull Consumer<SurfaceControlViewHost.SurfacePackage> consumer)110         void getSurfacePackage(@NonNull Consumer<SurfaceControlViewHost.SurfacePackage> consumer);
111     }
112 
113     @NonNull
114     private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
115         @Override
116         public void surfaceCreated(@NonNull SurfaceHolder holder) {
117             final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl();
118             surfaceControl.addOnReparentListener(mOnReparentListener);
119             mSurfaceControlCallback.onCreated(surfaceControl);
120         }
121 
122         @Override
123         public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
124                 int height) {
125             /* do nothing */
126         }
127 
128         @Override
129         public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
130             final SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl();
131             surfaceControl.removeOnReparentListener(mOnReparentListener);
132             mSurfaceControlCallback.onDestroyed(surfaceControl);
133         }
134     };
135 
136     @NonNull
137     private final SurfaceControl.OnReparentListener mOnReparentListener =
138             new SurfaceControl.OnReparentListener() {
139                 @Override
140                 public void onReparent(SurfaceControl.Transaction transaction,
141                         SurfaceControl parent) {
142                     final View parentSurfaceOwnerView = (parent != null)
143                             ? parent.getLocalOwnerView() : null;
144                     if (parentSurfaceOwnerView instanceof SurfaceView) {
145                         mParentSurfaceOwnerView = new WeakReference<>(
146                                 (SurfaceView) parentSurfaceOwnerView);
147                     } else {
148                         mParentSurfaceOwnerView = null;
149                     }
150                 }
151             };
152 
153     @NonNull
154     private final ViewTreeObserver.OnDrawListener mOnDrawListener =
155             new ViewTreeObserver.OnDrawListener() {
156         @Override
157         public void onDraw() {
158             computeParentPositionAndScale();
159             final int visibility = InlineContentView.this.isShown() ? VISIBLE : GONE;
160             mSurfaceView.setVisibility(visibility);
161         }
162     };
163 
164     @NonNull
165     private final SurfaceView mSurfaceView;
166 
167     @Nullable
168     private WeakReference<SurfaceView> mParentSurfaceOwnerView;
169 
170     @Nullable
171     private int[] mParentPosition;
172 
173     @Nullable
174     private PointF mParentScale;
175 
176     @Nullable
177     private SurfaceControlCallback mSurfaceControlCallback;
178 
179     @Nullable
180     private SurfacePackageUpdater mSurfacePackageUpdater;
181 
182     /**
183      * @inheritDoc
184      * @hide
185      */
InlineContentView(@onNull Context context)186     public InlineContentView(@NonNull Context context) {
187         this(context, null);
188     }
189 
190     /**
191      * @inheritDoc
192      * @hide
193      */
InlineContentView(@onNull Context context, @Nullable AttributeSet attrs)194     public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs) {
195         this(context, attrs, 0);
196     }
197 
198     /**
199      * @inheritDoc
200      * @hide
201      */
InlineContentView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)202     public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs,
203             int defStyleAttr) {
204         this(context, attrs, defStyleAttr, 0);
205         mSurfaceView.setEnableSurfaceClipping(true);
206     }
207 
208     /**
209      * Gets the surface control. If the surface is not created this method returns {@code null}.
210      *
211      * @return The surface control.
212      * @see #setSurfaceControlCallback(SurfaceControlCallback)
213      */
214     @Nullable
getSurfaceControl()215     public SurfaceControl getSurfaceControl() {
216         return mSurfaceView.getSurfaceControl();
217     }
218 
219     @Override
setClipBounds(Rect clipBounds)220     public void setClipBounds(Rect clipBounds) {
221         super.setClipBounds(clipBounds);
222         mSurfaceView.setClipBounds(clipBounds);
223     }
224 
225     /**
226      * @inheritDoc
227      * @hide
228      */
InlineContentView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)229     public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs,
230             int defStyleAttr, int defStyleRes) {
231         super(context, attrs, defStyleAttr, defStyleRes);
232         mSurfaceView = new SurfaceView(context, attrs, defStyleAttr, defStyleRes) {
233             @Override
234             protected void onSetSurfacePositionAndScaleRT(
235                     @NonNull SurfaceControl.Transaction transaction,
236                     @NonNull SurfaceControl surface, int positionLeft, int positionTop,
237                     float postScaleX, float postScaleY) {
238                 // If we have a parent position, we need to make our coordinates relative
239                 // to the parent in the rendering space.
240                 if (mParentPosition != null) {
241                     positionLeft = (int) ((positionLeft - mParentPosition[0]) / mParentScale.x);
242                     positionTop = (int) ((positionTop - mParentPosition[1]) / mParentScale.y);
243                 }
244 
245                 // Any scaling done to the parent or its predecessors would be applied
246                 // via the surfaces parent -> child relation, so we only propagate any
247                 // scaling set on the InlineContentView itself.
248                 postScaleX = InlineContentView.this.getScaleX();
249                 postScaleY = InlineContentView.this.getScaleY();
250 
251                 super.onSetSurfacePositionAndScaleRT(transaction, surface, positionLeft,
252                         positionTop, postScaleX, postScaleY);
253             }
254         };
255         mSurfaceView.setZOrderOnTop(true);
256         mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
257         addView(mSurfaceView);
258         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
259     }
260 
261     /**
262      * Sets the embedded UI provider.
263      *
264      * @hide
265      */
266     @TestApi
setChildSurfacePackageUpdater( @ullable SurfacePackageUpdater surfacePackageUpdater)267     public void setChildSurfacePackageUpdater(
268             @Nullable SurfacePackageUpdater surfacePackageUpdater) {
269         mSurfacePackageUpdater = surfacePackageUpdater;
270     }
271 
272     @Override
onAttachedToWindow()273     protected void onAttachedToWindow() {
274         if (DEBUG) Log.v(TAG, "onAttachedToWindow");
275         super.onAttachedToWindow();
276         if (mSurfacePackageUpdater != null) {
277             mSurfacePackageUpdater.getSurfacePackage(
278                     sp -> {
279                         if (DEBUG) Log.v(TAG, "Received new SurfacePackage");
280                         if (getViewRootImpl() != null) {
281                             mSurfaceView.setChildSurfacePackage(sp);
282                         }
283                     });
284         }
285 
286         mSurfaceView.setVisibility(getVisibility());
287         getViewTreeObserver().addOnDrawListener(mOnDrawListener);
288     }
289 
290     @Override
onDetachedFromWindow()291     protected void onDetachedFromWindow() {
292         if (DEBUG) Log.v(TAG, "onDetachedFromWindow");
293         super.onDetachedFromWindow();
294         if (mSurfacePackageUpdater != null) {
295             mSurfacePackageUpdater.onSurfacePackageReleased();
296         }
297 
298         getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
299         mSurfaceView.setVisibility(View.GONE);
300     }
301 
302     @Override
onLayout(boolean changed, int l, int t, int r, int b)303     public void onLayout(boolean changed, int l, int t, int r, int b) {
304         mSurfaceView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
305     }
306 
307     /**
308      * Sets a callback to observe the lifecycle of the surface control for managing the backing
309      * surface.
310      *
311      * @param callback The callback to set or {@code null} to clear.
312      */
setSurfaceControlCallback(@ullable SurfaceControlCallback callback)313     public void setSurfaceControlCallback(@Nullable SurfaceControlCallback callback) {
314         if (mSurfaceControlCallback != null) {
315             mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
316         }
317         mSurfaceControlCallback = callback;
318         if (mSurfaceControlCallback != null) {
319             mSurfaceView.getHolder().addCallback(mSurfaceCallback);
320         }
321     }
322 
323     /**
324      * @return Whether the surface backing this view appears on top of its parent.
325      * @see #setZOrderedOnTop(boolean)
326      */
isZOrderedOnTop()327     public boolean isZOrderedOnTop() {
328         return mSurfaceView.isZOrderedOnTop();
329     }
330 
331     /**
332      * Controls whether the backing surface is placed on top of this view's window. Normally, it is
333      * placed on top of the window, to allow interaction with the inlined UI. Via this method, you
334      * can place the surface below the window. This means that all of the contents of the window
335      * this view is in will be visible on top of its surface.
336      *
337      * <p> The Z ordering can be changed dynamically if the backing surface is
338      * created, otherwise the ordering would be applied at surface construction time.
339      *
340      * @param onTop Whether to show the surface on top of this view's window.
341      * @see #isZOrderedOnTop()
342      */
setZOrderedOnTop(boolean onTop)343     public boolean setZOrderedOnTop(boolean onTop) {
344         return mSurfaceView.setZOrderedOnTop(onTop, /*allowDynamicChange*/ true);
345     }
346 
347 
computeParentPositionAndScale()348     private void computeParentPositionAndScale() {
349         boolean contentPositionOrScaleChanged = false;
350 
351         // This method can be called on the UI or render thread but for the cases
352         // it is called these threads are not running concurrently, so no need to lock.
353         final SurfaceView parentSurfaceOwnerView = (mParentSurfaceOwnerView != null)
354                 ? mParentSurfaceOwnerView.get() : null;
355 
356         if (parentSurfaceOwnerView != null) {
357             if (mParentPosition == null) {
358                 mParentPosition = new int[2];
359             }
360             final int oldParentPositionX = mParentPosition[0];
361             final int oldParentPositionY = mParentPosition[1];
362             parentSurfaceOwnerView.getLocationInSurface(mParentPosition);
363             if (oldParentPositionX != mParentPosition[0]
364                     || oldParentPositionY != mParentPosition[1]) {
365                 contentPositionOrScaleChanged = true;
366             }
367 
368             if (mParentScale == null) {
369                 mParentScale = new PointF();
370             }
371 
372             final float lastParentSurfaceWidth = parentSurfaceOwnerView
373                     .getSurfaceRenderPosition().width();
374             final float oldParentScaleX = mParentScale.x;
375             if (lastParentSurfaceWidth > 0) {
376                 mParentScale.x = lastParentSurfaceWidth /
377                         (float) parentSurfaceOwnerView.getWidth();
378             } else {
379                 mParentScale.x = 1.0f;
380             }
381             if (!contentPositionOrScaleChanged
382                     && Float.compare(oldParentScaleX, mParentScale.x) != 0) {
383                 contentPositionOrScaleChanged = true;
384             }
385 
386             final float lastParentSurfaceHeight = parentSurfaceOwnerView
387                     .getSurfaceRenderPosition().height();
388             final float oldParentScaleY = mParentScale.y;
389             if (lastParentSurfaceHeight > 0) {
390                 mParentScale.y = lastParentSurfaceHeight
391                         / (float) parentSurfaceOwnerView.getHeight();
392             } else {
393                 mParentScale.y = 1.0f;
394             }
395             if (!contentPositionOrScaleChanged
396                     && Float.compare(oldParentScaleY, mParentScale.y) != 0) {
397                 contentPositionOrScaleChanged = true;
398             }
399         } else if (mParentPosition != null || mParentScale != null) {
400             contentPositionOrScaleChanged = true;
401             mParentPosition = null;
402             mParentScale = null;
403         }
404 
405         if (contentPositionOrScaleChanged) {
406             mSurfaceView.requestUpdateSurfacePositionAndScale();
407         }
408     }
409 }
410