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             // b/219807628
234             @Override
235             protected void onSetSurfacePositionAndScale(
236                     @NonNull SurfaceControl.Transaction transaction,
237                     @NonNull SurfaceControl surface, int positionLeft, int positionTop,
238                     float postScaleX, float postScaleY) {
239                 // If we have a parent position, we need to make our coordinates relative
240                 // to the parent in the rendering space.
241                 if (mParentPosition != null) {
242                     positionLeft = (int) ((positionLeft - mParentPosition[0]) / mParentScale.x);
243                     positionTop = (int) ((positionTop - mParentPosition[1]) / mParentScale.y);
244                 }
245 
246                 // Any scaling done to the parent or its predecessors would be applied
247                 // via the surfaces parent -> child relation, so we only propagate any
248                 // scaling set on the InlineContentView itself.
249                 postScaleX = InlineContentView.this.getScaleX();
250                 postScaleY = InlineContentView.this.getScaleY();
251 
252                 super.onSetSurfacePositionAndScale(transaction, surface, positionLeft,
253                         positionTop, postScaleX, postScaleY);
254             }
255         };
256         mSurfaceView.setZOrderOnTop(true);
257         mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
258         addView(mSurfaceView);
259         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
260     }
261 
262     /**
263      * Sets the embedded UI provider.
264      *
265      * @hide
266      */
267     @TestApi
setChildSurfacePackageUpdater( @ullable SurfacePackageUpdater surfacePackageUpdater)268     public void setChildSurfacePackageUpdater(
269             @Nullable SurfacePackageUpdater surfacePackageUpdater) {
270         mSurfacePackageUpdater = surfacePackageUpdater;
271     }
272 
273     @Override
onAttachedToWindow()274     protected void onAttachedToWindow() {
275         if (DEBUG) Log.v(TAG, "onAttachedToWindow");
276         super.onAttachedToWindow();
277         if (mSurfacePackageUpdater != null) {
278             mSurfacePackageUpdater.getSurfacePackage(
279                     sp -> {
280                         if (DEBUG) Log.v(TAG, "Received new SurfacePackage");
281                         if (getViewRootImpl() != null) {
282                             mSurfaceView.setChildSurfacePackage(sp);
283                         }
284                     });
285         }
286 
287         mSurfaceView.setVisibility(getVisibility());
288         getViewTreeObserver().addOnDrawListener(mOnDrawListener);
289     }
290 
291     @Override
onDetachedFromWindow()292     protected void onDetachedFromWindow() {
293         if (DEBUG) Log.v(TAG, "onDetachedFromWindow");
294         super.onDetachedFromWindow();
295         if (mSurfacePackageUpdater != null) {
296             mSurfacePackageUpdater.onSurfacePackageReleased();
297         }
298 
299         getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
300         mSurfaceView.setVisibility(View.GONE);
301     }
302 
303     @Override
onLayout(boolean changed, int l, int t, int r, int b)304     public void onLayout(boolean changed, int l, int t, int r, int b) {
305         mSurfaceView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
306     }
307 
308     /**
309      * Sets a callback to observe the lifecycle of the surface control for managing the backing
310      * surface.
311      *
312      * @param callback The callback to set or {@code null} to clear.
313      */
setSurfaceControlCallback(@ullable SurfaceControlCallback callback)314     public void setSurfaceControlCallback(@Nullable SurfaceControlCallback callback) {
315         if (mSurfaceControlCallback != null) {
316             mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
317         }
318         mSurfaceControlCallback = callback;
319         if (mSurfaceControlCallback != null) {
320             mSurfaceView.getHolder().addCallback(mSurfaceCallback);
321         }
322     }
323 
324     /**
325      * @return Whether the surface backing this view appears on top of its parent.
326      * @see #setZOrderedOnTop(boolean)
327      */
isZOrderedOnTop()328     public boolean isZOrderedOnTop() {
329         return mSurfaceView.isZOrderedOnTop();
330     }
331 
332     /**
333      * Controls whether the backing surface is placed on top of this view's window. Normally, it is
334      * placed on top of the window, to allow interaction with the inlined UI. Via this method, you
335      * can place the surface below the window. This means that all of the contents of the window
336      * this view is in will be visible on top of its surface.
337      *
338      * <p> The Z ordering can be changed dynamically if the backing surface is
339      * created, otherwise the ordering would be applied at surface construction time.
340      *
341      * @param onTop Whether to show the surface on top of this view's window.
342      * @see #isZOrderedOnTop()
343      */
setZOrderedOnTop(boolean onTop)344     public boolean setZOrderedOnTop(boolean onTop) {
345         return mSurfaceView.setZOrderedOnTop(onTop, /*allowDynamicChange*/ true);
346     }
347 
348 
computeParentPositionAndScale()349     private void computeParentPositionAndScale() {
350         boolean contentPositionOrScaleChanged = false;
351 
352         // This method can be called on the UI or render thread but for the cases
353         // it is called these threads are not running concurrently, so no need to lock.
354         final SurfaceView parentSurfaceOwnerView = (mParentSurfaceOwnerView != null)
355                 ? mParentSurfaceOwnerView.get() : null;
356 
357         if (parentSurfaceOwnerView != null) {
358             if (mParentPosition == null) {
359                 mParentPosition = new int[2];
360             }
361             final int oldParentPositionX = mParentPosition[0];
362             final int oldParentPositionY = mParentPosition[1];
363             parentSurfaceOwnerView.getLocationInSurface(mParentPosition);
364             if (oldParentPositionX != mParentPosition[0]
365                     || oldParentPositionY != mParentPosition[1]) {
366                 contentPositionOrScaleChanged = true;
367             }
368 
369             if (mParentScale == null) {
370                 mParentScale = new PointF();
371             }
372 
373             final float lastParentSurfaceWidth = parentSurfaceOwnerView
374                     .getSurfaceRenderPosition().width();
375             final float oldParentScaleX = mParentScale.x;
376             if (lastParentSurfaceWidth > 0) {
377                 mParentScale.x = lastParentSurfaceWidth /
378                         (float) parentSurfaceOwnerView.getWidth();
379             } else {
380                 mParentScale.x = 1.0f;
381             }
382             if (!contentPositionOrScaleChanged
383                     && Float.compare(oldParentScaleX, mParentScale.x) != 0) {
384                 contentPositionOrScaleChanged = true;
385             }
386 
387             final float lastParentSurfaceHeight = parentSurfaceOwnerView
388                     .getSurfaceRenderPosition().height();
389             final float oldParentScaleY = mParentScale.y;
390             if (lastParentSurfaceHeight > 0) {
391                 mParentScale.y = lastParentSurfaceHeight
392                         / (float) parentSurfaceOwnerView.getHeight();
393             } else {
394                 mParentScale.y = 1.0f;
395             }
396             if (!contentPositionOrScaleChanged
397                     && Float.compare(oldParentScaleY, mParentScale.y) != 0) {
398                 contentPositionOrScaleChanged = true;
399             }
400         } else if (mParentPosition != null || mParentScale != null) {
401             contentPositionOrScaleChanged = true;
402             mParentPosition = null;
403             mParentScale = null;
404         }
405 
406         if (contentPositionOrScaleChanged) {
407             mSurfaceView.requestUpdateSurfacePositionAndScale();
408         }
409     }
410 }
411