1 /*
2  * Copyright (C) 2010 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 com.android.layoutlib.bridge.impl;
18 
19 import com.android.ide.common.rendering.api.AdapterBinding;
20 import com.android.ide.common.rendering.api.HardwareConfig;
21 import com.android.ide.common.rendering.api.ILayoutPullParser;
22 import com.android.ide.common.rendering.api.LayoutLog;
23 import com.android.ide.common.rendering.api.LayoutlibCallback;
24 import com.android.ide.common.rendering.api.RenderSession;
25 import com.android.ide.common.rendering.api.ResourceReference;
26 import com.android.ide.common.rendering.api.ResourceValue;
27 import com.android.ide.common.rendering.api.Result;
28 import com.android.ide.common.rendering.api.SessionParams;
29 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
30 import com.android.ide.common.rendering.api.ViewInfo;
31 import com.android.ide.common.rendering.api.ViewType;
32 import com.android.internal.view.menu.ActionMenuItemView;
33 import com.android.internal.view.menu.BridgeMenuItemImpl;
34 import com.android.internal.view.menu.IconMenuItemView;
35 import com.android.internal.view.menu.ListMenuItemView;
36 import com.android.internal.view.menu.MenuItemImpl;
37 import com.android.internal.view.menu.MenuView;
38 import com.android.layoutlib.bridge.Bridge;
39 import com.android.layoutlib.bridge.android.BridgeContext;
40 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
41 import com.android.layoutlib.bridge.android.RenderParamsFlags;
42 import com.android.layoutlib.bridge.android.graphics.NopCanvas;
43 import com.android.layoutlib.bridge.android.support.DesignLibUtil;
44 import com.android.layoutlib.bridge.android.support.FragmentTabHostUtil;
45 import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
46 import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
47 import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
48 import com.android.tools.layoutlib.java.System_Delegate;
49 import com.android.util.Pair;
50 
51 import android.annotation.NonNull;
52 import android.annotation.Nullable;
53 import android.app.Fragment_Delegate;
54 import android.graphics.Bitmap;
55 import android.graphics.Bitmap_Delegate;
56 import android.graphics.Canvas;
57 import android.graphics.NinePatch_Delegate;
58 import android.os.Looper;
59 import android.preference.Preference_Delegate;
60 import android.view.AttachInfo_Accessor;
61 import android.view.BridgeInflater;
62 import android.view.Choreographer_Delegate;
63 import android.view.View;
64 import android.view.View.MeasureSpec;
65 import android.view.ViewGroup;
66 import android.view.ViewGroup.LayoutParams;
67 import android.view.ViewGroup.MarginLayoutParams;
68 import android.view.ViewParent;
69 import android.widget.AbsListView;
70 import android.widget.AbsSpinner;
71 import android.widget.ActionMenuView;
72 import android.widget.AdapterView;
73 import android.widget.ExpandableListView;
74 import android.widget.FrameLayout;
75 import android.widget.LinearLayout;
76 import android.widget.ListView;
77 import android.widget.QuickContactBadge;
78 import android.widget.TabHost;
79 import android.widget.TabHost.TabSpec;
80 import android.widget.TabWidget;
81 
82 import java.awt.AlphaComposite;
83 import java.awt.Color;
84 import java.awt.Graphics2D;
85 import java.awt.image.BufferedImage;
86 import java.util.ArrayList;
87 import java.util.IdentityHashMap;
88 import java.util.List;
89 import java.util.Map;
90 
91 import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION;
92 import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED;
93 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
94 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
95 import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf;
96 
97 /**
98  * Class implementing the render session.
99  * <p/>
100  * A session is a stateful representation of a layout file. It is initialized with data coming
101  * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then
102  * be done on the layout.
103  */
104 public class RenderSessionImpl extends RenderAction<SessionParams> {
105 
106     private static final Canvas NOP_CANVAS = new NopCanvas();
107 
108     // scene state
109     private RenderSession mScene;
110     private BridgeXmlBlockParser mBlockParser;
111     private BridgeInflater mInflater;
112     private ViewGroup mViewRoot;
113     private FrameLayout mContentRoot;
114     private Canvas mCanvas;
115     private int mMeasuredScreenWidth = -1;
116     private int mMeasuredScreenHeight = -1;
117     private boolean mIsAlphaChannelImage;
118     /** If >= 0, a frame will be executed */
119     private long mElapsedFrameTimeNanos = -1;
120     /** True if one frame has been already executed to start the animations */
121     private boolean mFirstFrameExecuted = false;
122 
123     // information being returned through the API
124     private BufferedImage mImage;
125     private List<ViewInfo> mViewInfoList;
126     private List<ViewInfo> mSystemViewInfoList;
127     private Layout.Builder mLayoutBuilder;
128     private boolean mNewRenderSize;
129 
130     private static final class PostInflateException extends Exception {
131         private static final long serialVersionUID = 1L;
132 
PostInflateException(String message)133         private PostInflateException(String message) {
134             super(message);
135         }
136     }
137 
138     /**
139      * Creates a layout scene with all the information coming from the layout bridge API.
140      * <p>
141      * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init(long)},
142      * which act as a
143      * call to {@link RenderSessionImpl#acquire(long)}
144      *
145      * @see Bridge#createSession(SessionParams)
146      */
RenderSessionImpl(SessionParams params)147     public RenderSessionImpl(SessionParams params) {
148         super(new SessionParams(params));
149     }
150 
151     /**
152      * Initializes and acquires the scene, creating various Android objects such as context,
153      * inflater, and parser.
154      *
155      * @param timeout the time to wait if another rendering is happening.
156      *
157      * @return whether the scene was prepared
158      *
159      * @see #acquire(long)
160      * @see #release()
161      */
162     @Override
init(long timeout)163     public Result init(long timeout) {
164         Result result = super.init(timeout);
165         if (!result.isSuccess()) {
166             return result;
167         }
168 
169         SessionParams params = getParams();
170         BridgeContext context = getContext();
171 
172         // use default of true in case it's not found to use alpha by default
173         mIsAlphaChannelImage =
174                 ResourceHelper.getBooleanThemeFrameworkAttrValue(params.getResources(),
175                         "windowIsFloating", true);
176 
177         mLayoutBuilder = new Layout.Builder(params, context);
178 
179         // build the inflater and parser.
180         mInflater = new BridgeInflater(context, params.getLayoutlibCallback());
181         context.setBridgeInflater(mInflater);
182 
183         ILayoutPullParser layoutParser = params.getLayoutDescription();
184         mBlockParser = new BridgeXmlBlockParser(layoutParser, context, layoutParser.getLayoutNamespace());
185 
186         return SUCCESS.createResult();
187     }
188 
189     /**
190      * Measures the the current layout if needed (see {@link #invalidateRenderingSize}).
191      */
measureLayout(@onNull SessionParams params)192     private void measureLayout(@NonNull SessionParams params) {
193         // only do the screen measure when needed.
194         if (mMeasuredScreenWidth != -1) {
195             return;
196         }
197 
198         RenderingMode renderingMode = params.getRenderingMode();
199         HardwareConfig hardwareConfig = params.getHardwareConfig();
200 
201         mNewRenderSize = true;
202         mMeasuredScreenWidth = hardwareConfig.getScreenWidth();
203         mMeasuredScreenHeight = hardwareConfig.getScreenHeight();
204 
205         if (renderingMode != RenderingMode.NORMAL) {
206             int widthMeasureSpecMode = renderingMode.isHorizExpand() ?
207                     MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
208                     : MeasureSpec.EXACTLY;
209             int heightMeasureSpecMode = renderingMode.isVertExpand() ?
210                     MeasureSpec.UNSPECIFIED // this lets us know the actual needed size
211                     : MeasureSpec.EXACTLY;
212 
213             // We used to compare the measured size of the content to the screen size but
214             // this does not work anymore due to the 2 following issues:
215             // - If the content is in a decor (system bar, title/action bar), the root view
216             //   will not resize even with the UNSPECIFIED because of the embedded layout.
217             // - If there is no decor, but a dialog frame, then the dialog padding prevents
218             //   comparing the size of the content to the screen frame (as it would not
219             //   take into account the dialog padding).
220 
221             // The solution is to first get the content size in a normal rendering, inside
222             // the decor or the dialog padding.
223             // Then measure only the content with UNSPECIFIED to see the size difference
224             // and apply this to the screen size.
225 
226             View measuredView = mContentRoot.getChildAt(0);
227 
228             // first measure the full layout, with EXACTLY to get the size of the
229             // content as it is inside the decor/dialog
230             @SuppressWarnings("deprecation")
231             Pair<Integer, Integer> exactMeasure = measureView(
232                     mViewRoot, measuredView,
233                     mMeasuredScreenWidth, MeasureSpec.EXACTLY,
234                     mMeasuredScreenHeight, MeasureSpec.EXACTLY);
235 
236             // now measure the content only using UNSPECIFIED (where applicable, based on
237             // the rendering mode). This will give us the size the content needs.
238             @SuppressWarnings("deprecation")
239             Pair<Integer, Integer> result = measureView(
240                     mContentRoot, mContentRoot.getChildAt(0),
241                     mMeasuredScreenWidth, widthMeasureSpecMode,
242                     mMeasuredScreenHeight, heightMeasureSpecMode);
243 
244             // If measuredView is not null, exactMeasure nor result will be null.
245             assert exactMeasure != null;
246             assert result != null;
247 
248             // now look at the difference and add what is needed.
249             if (renderingMode.isHorizExpand()) {
250                 int measuredWidth = exactMeasure.getFirst();
251                 int neededWidth = result.getFirst();
252                 if (neededWidth > measuredWidth) {
253                     mMeasuredScreenWidth += neededWidth - measuredWidth;
254                 }
255                 if (mMeasuredScreenWidth < measuredWidth) {
256                     // If the screen width is less than the exact measured width,
257                     // expand to match.
258                     mMeasuredScreenWidth = measuredWidth;
259                 }
260             }
261 
262             if (renderingMode.isVertExpand()) {
263                 int measuredHeight = exactMeasure.getSecond();
264                 int neededHeight = result.getSecond();
265                 if (neededHeight > measuredHeight) {
266                     mMeasuredScreenHeight += neededHeight - measuredHeight;
267                 }
268                 if (mMeasuredScreenHeight < measuredHeight) {
269                     // If the screen height is less than the exact measured height,
270                     // expand to match.
271                     mMeasuredScreenHeight = measuredHeight;
272                 }
273             }
274         }
275     }
276 
277     /**
278      * Inflates the layout.
279      * <p>
280      * {@link #acquire(long)} must have been called before this.
281      *
282      * @throws IllegalStateException if the current context is different than the one owned by
283      *      the scene, or if {@link #init(long)} was not called.
284      */
inflate()285     public Result inflate() {
286         checkLock();
287 
288         try {
289             mViewRoot = new Layout(mLayoutBuilder);
290             mLayoutBuilder = null;  // Done with the builder.
291             mContentRoot = ((Layout) mViewRoot).getContentRoot();
292             SessionParams params = getParams();
293             BridgeContext context = getContext();
294 
295             if (Bridge.isLocaleRtl(params.getLocale())) {
296                 if (!params.isRtlSupported()) {
297                     Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_ENABLED,
298                             "You are using a right-to-left " +
299                                     "(RTL) locale but RTL is not enabled", null);
300                 } else if (params.getSimulatedPlatformVersion() !=0 &&
301                         params.getSimulatedPlatformVersion() < 17) {
302                     // This will render ok because we are using the latest layoutlib but at least
303                     // warn the user that this might fail in a real device.
304                     Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " +
305                             "right-to-left " +
306                             "(RTL) locale but RTL is not supported for API level < 17", null);
307                 }
308             }
309 
310             // Sets the project callback (custom view loader) to the fragment delegate so that
311             // it can instantiate the custom Fragment.
312             Fragment_Delegate.setLayoutlibCallback(params.getLayoutlibCallback());
313 
314             String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG);
315             boolean isPreference = "PreferenceScreen".equals(rootTag) ||
316                     SupportPreferencesUtil.isSupportRootTag(rootTag);
317             View view;
318             if (isPreference) {
319                 // First try to use the support library inflater. If something fails, fallback
320                 // to the system preference inflater.
321                 view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser,
322                         mContentRoot);
323                 if (view == null) {
324                     view = Preference_Delegate.inflatePreference(getContext(), mBlockParser,
325                             mContentRoot);
326                 }
327             } else {
328                 view = mInflater.inflate(mBlockParser, mContentRoot);
329             }
330 
331             // done with the parser, pop it.
332             context.popParser();
333 
334             Fragment_Delegate.setLayoutlibCallback(null);
335 
336             // set the AttachInfo on the root view.
337             AttachInfo_Accessor.setAttachInfo(mViewRoot);
338 
339             // post-inflate process. For now this supports TabHost/TabWidget
340             postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null);
341             mInflater.onDoneInflation();
342 
343             setActiveToolbar(view, context, params);
344 
345             measureLayout(params);
346             measureView(mViewRoot, null /*measuredView*/,
347                     mMeasuredScreenWidth, MeasureSpec.EXACTLY,
348                     mMeasuredScreenHeight, MeasureSpec.EXACTLY);
349             mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
350             mSystemViewInfoList =
351                     visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
352                     false);
353 
354             Choreographer_Delegate.clearFrames();
355 
356             return SUCCESS.createResult();
357         } catch (PostInflateException e) {
358             return ERROR_INFLATION.createResult(e.getMessage(), e);
359         } catch (Throwable e) {
360             // get the real cause of the exception.
361             Throwable t = e;
362             while (t.getCause() != null) {
363                 t = t.getCause();
364             }
365 
366             return ERROR_INFLATION.createResult(t.getMessage(), t);
367         }
368     }
369 
370     /**
371      * Sets the time for which the next frame will be selected. The time is the elapsed time from
372      * the current system nanos time. You
373      */
setElapsedFrameTimeNanos(long nanos)374     public void setElapsedFrameTimeNanos(long nanos) {
375         mElapsedFrameTimeNanos = nanos;
376     }
377 
378     /**
379      * Runs a layout pass for the given view root
380      */
doLayout(@onNull BridgeContext context, @NonNull ViewGroup viewRoot, int width, int height)381     private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
382             int width, int height) {
383         // measure again with the size we need
384         // This must always be done before the call to layout
385         measureView(viewRoot, null /*measuredView*/,
386                 width, MeasureSpec.EXACTLY,
387                 height, MeasureSpec.EXACTLY);
388 
389         // now do the layout.
390         viewRoot.layout(0, 0, width, height);
391         handleScrolling(context, viewRoot);
392     }
393 
394     /**
395      * Renders the given view hierarchy to the passed canvas and returns the result of the render
396      * operation.
397      * @param canvas an optional canvas to render the views to. If null, only the measure and
398      * layout steps will be executed.
399      */
renderAndBuildResult(@onNull ViewGroup viewRoot, @Nullable Canvas canvas)400     private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) {
401         if (canvas == null) {
402             return SUCCESS.createResult();
403         }
404 
405         AttachInfo_Accessor.dispatchOnPreDraw(viewRoot);
406         viewRoot.draw(canvas);
407 
408         return SUCCESS.createResult();
409     }
410 
411     /**
412      * Renders the scene.
413      * <p>
414      * {@link #acquire(long)} must have been called before this.
415      *
416      * @param freshRender whether the render is a new one and should erase the existing bitmap (in
417      *      the case where bitmaps are reused). This is typically needed when not playing
418      *      animations.)
419      *
420      * @throws IllegalStateException if the current context is different than the one owned by
421      *      the scene, or if {@link #acquire(long)} was not called.
422      *
423      * @see SessionParams#getRenderingMode()
424      * @see RenderSession#render(long)
425      */
render(boolean freshRender)426     public Result render(boolean freshRender) {
427         return renderAndBuildResult(freshRender, false);
428     }
429 
430     /**
431      * Measures the layout
432      * <p>
433      * {@link #acquire(long)} must have been called before this.
434      *
435      * @throws IllegalStateException if the current context is different than the one owned by
436      *      the scene, or if {@link #acquire(long)} was not called.
437      *
438      * @see SessionParams#getRenderingMode()
439      * @see RenderSession#render(long)
440      */
measure()441     public Result measure() {
442         return renderAndBuildResult(false, true);
443     }
444 
445     /**
446      * Renders the scene.
447      * <p>
448      * {@link #acquire(long)} must have been called before this.
449      *
450      * @param freshRender whether the render is a new one and should erase the existing bitmap (in
451      *      the case where bitmaps are reused). This is typically needed when not playing
452      *      animations.)
453      *
454      * @throws IllegalStateException if the current context is different than the one owned by
455      *      the scene, or if {@link #acquire(long)} was not called.
456      *
457      * @see SessionParams#getRenderingMode()
458      * @see RenderSession#render(long)
459      */
renderAndBuildResult(boolean freshRender, boolean onlyMeasure)460     private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) {
461         checkLock();
462 
463         SessionParams params = getParams();
464 
465         try {
466             if (mViewRoot == null) {
467                 return ERROR_NOT_INFLATED.createResult();
468             }
469 
470             measureLayout(params);
471 
472             HardwareConfig hardwareConfig = params.getHardwareConfig();
473             Result renderResult = SUCCESS.createResult();
474             if (onlyMeasure) {
475                 // delete the canvas and image to reset them on the next full rendering
476                 mImage = null;
477                 mCanvas = null;
478                 doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
479             } else {
480                 // draw the views
481                 // create the BufferedImage into which the layout will be rendered.
482                 boolean newImage = false;
483 
484                 // When disableBitmapCaching is true, we do not reuse mImage and
485                 // we create a new one in every render.
486                 // This is useful when mImage is just a wrapper of Graphics2D so
487                 // it doesn't get cached.
488                 boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag(
489                     RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING));
490 
491                 if (mNewRenderSize || mCanvas == null || disableBitmapCaching) {
492                     mNewRenderSize = false;
493                     if (params.getImageFactory() != null) {
494                         mImage = params.getImageFactory().getImage(
495                                 mMeasuredScreenWidth,
496                                 mMeasuredScreenHeight);
497                     } else {
498                         mImage = new BufferedImage(
499                                 mMeasuredScreenWidth,
500                                 mMeasuredScreenHeight,
501                                 BufferedImage.TYPE_INT_ARGB);
502                         newImage = true;
503                     }
504 
505                     if (params.isBgColorOverridden()) {
506                         // since we override the content, it's the same as if it was a new image.
507                         newImage = true;
508                         Graphics2D gc = mImage.createGraphics();
509                         gc.setColor(new Color(params.getOverrideBgColor(), true));
510                         gc.setComposite(AlphaComposite.Src);
511                         gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight);
512                         gc.dispose();
513                     }
514 
515                     // create an Android bitmap around the BufferedImage
516                     Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage,
517                             true /*isMutable*/, hardwareConfig.getDensity());
518 
519                     if (mCanvas == null) {
520                         // create a Canvas around the Android bitmap
521                         mCanvas = new Canvas(bitmap);
522                     } else {
523                         mCanvas.setBitmap(bitmap);
524                     }
525 
526                     boolean enableImageResizing =
527                             mImage.getWidth() != mMeasuredScreenWidth &&
528                             mImage.getHeight() != mMeasuredScreenHeight &&
529                             Boolean.TRUE.equals(params.getFlag(
530                                     RenderParamsFlags.FLAG_KEY_RESULT_IMAGE_AUTO_SCALE));
531 
532                     if (enableImageResizing) {
533                         float scaleX = (float)mImage.getWidth() / mMeasuredScreenWidth;
534                         float scaleY = (float)mImage.getHeight() / mMeasuredScreenHeight;
535                         mCanvas.scale(scaleX, scaleY);
536                     }
537 
538                     mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue());
539                 }
540 
541                 if (freshRender && !newImage) {
542                     Graphics2D gc = mImage.createGraphics();
543                     gc.setComposite(AlphaComposite.Src);
544 
545                     gc.setColor(new Color(0x00000000, true));
546                     gc.fillRect(0, 0,
547                             mMeasuredScreenWidth, mMeasuredScreenHeight);
548 
549                     // done
550                     gc.dispose();
551                 }
552 
553                 doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
554                 if (mElapsedFrameTimeNanos >= 0) {
555                     long initialTime = System_Delegate.nanoTime();
556                     if (!mFirstFrameExecuted) {
557                         // We need to run an initial draw call to initialize the animations
558                         renderAndBuildResult(mViewRoot, NOP_CANVAS);
559 
560                         // The first frame will initialize the animations
561                         Choreographer_Delegate.doFrame(initialTime);
562                         mFirstFrameExecuted = true;
563                     }
564                     // Second frame will move the animations
565                     Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos);
566                 }
567                 renderResult = renderAndBuildResult(mViewRoot, mCanvas);
568             }
569 
570             mSystemViewInfoList =
571                     visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(),
572                     false);
573 
574             // success!
575             return renderResult;
576         } catch (Throwable e) {
577             // get the real cause of the exception.
578             Throwable t = e;
579             while (t.getCause() != null) {
580                 t = t.getCause();
581             }
582 
583             return ERROR_UNKNOWN.createResult(t.getMessage(), t);
584         }
585     }
586 
587     /**
588      * Executes {@link View#measure(int, int)} on a given view with the given parameters (used
589      * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}.
590      *
591      * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height)
592      * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}).
593      *
594      * @param viewToMeasure the view on which to execute measure().
595      * @param measuredView if non null, the view to query for its measured width/height.
596      * @param width the width to use in the MeasureSpec.
597      * @param widthMode the MeasureSpec mode to use for the width.
598      * @param height the height to use in the MeasureSpec.
599      * @param heightMode the MeasureSpec mode to use for the height.
600      * @return the measured width/height if measuredView is non-null, null otherwise.
601      */
602     @SuppressWarnings("deprecation")  // For the use of Pair
measureView(ViewGroup viewToMeasure, View measuredView, int width, int widthMode, int height, int heightMode)603     private static Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView,
604             int width, int widthMode, int height, int heightMode) {
605         int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode);
606         int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode);
607         viewToMeasure.measure(w_spec, h_spec);
608 
609         if (measuredView != null) {
610             return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight());
611         }
612 
613         return null;
614     }
615 
616     /**
617      * Post process on a view hierarchy that was just inflated.
618      * <p/>
619      * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the
620      * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically
621      * based on the content of the {@link FrameLayout}.
622      * @param view the root view to process.
623      * @param layoutlibCallback callback to the project.
624      * @param skip the view and it's children are not processed.
625      */
626     @SuppressWarnings("deprecation")  // For the use of Pair
postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip)627     private void postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip)
628             throws PostInflateException {
629         if (view == skip) {
630             return;
631         }
632         if (view instanceof TabHost) {
633             setupTabHost((TabHost) view, layoutlibCallback);
634         } else if (view instanceof QuickContactBadge) {
635             QuickContactBadge badge = (QuickContactBadge) view;
636             badge.setImageToDefault();
637         } else if (view instanceof AdapterView<?>) {
638             // get the view ID.
639             int id = view.getId();
640 
641             BridgeContext context = getContext();
642 
643             // get a ResourceReference from the integer ID.
644             ResourceReference listRef = context.resolveId(id);
645 
646             if (listRef != null) {
647                 SessionParams params = getParams();
648                 AdapterBinding binding = params.getAdapterBindings().get(listRef);
649 
650                 // if there was no adapter binding, trying to get it from the call back.
651                 if (binding == null) {
652                     binding = layoutlibCallback.getAdapterBinding(
653                             listRef, context.getViewKey(view), view);
654                 }
655 
656                 if (binding != null) {
657 
658                     if (view instanceof AbsListView) {
659                         if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) &&
660                                 view instanceof ListView) {
661                             ListView list = (ListView) view;
662 
663                             boolean skipCallbackParser = false;
664 
665                             int count = binding.getHeaderCount();
666                             for (int i = 0; i < count; i++) {
667                                 Pair<View, Boolean> pair = context.inflateView(
668                                         binding.getHeaderAt(i),
669                                         list, false, skipCallbackParser);
670                                 if (pair.getFirst() != null) {
671                                     list.addHeaderView(pair.getFirst());
672                                 }
673 
674                                 skipCallbackParser |= pair.getSecond();
675                             }
676 
677                             count = binding.getFooterCount();
678                             for (int i = 0; i < count; i++) {
679                                 Pair<View, Boolean> pair = context.inflateView(
680                                         binding.getFooterAt(i),
681                                         list, false, skipCallbackParser);
682                                 if (pair.getFirst() != null) {
683                                     list.addFooterView(pair.getFirst());
684                                 }
685 
686                                 skipCallbackParser |= pair.getSecond();
687                             }
688                         }
689 
690                         if (view instanceof ExpandableListView) {
691                             ((ExpandableListView) view).setAdapter(
692                                     new FakeExpandableAdapter(listRef, binding, layoutlibCallback));
693                         } else {
694                             ((AbsListView) view).setAdapter(
695                                     new FakeAdapter(listRef, binding, layoutlibCallback));
696                         }
697                     } else if (view instanceof AbsSpinner) {
698                         ((AbsSpinner) view).setAdapter(
699                                 new FakeAdapter(listRef, binding, layoutlibCallback));
700                     }
701                 }
702             }
703         } else if (view instanceof ViewGroup) {
704             mInflater.postInflateProcess(view);
705             ViewGroup group = (ViewGroup) view;
706             final int count = group.getChildCount();
707             for (int c = 0; c < count; c++) {
708                 View child = group.getChildAt(c);
709                 postInflateProcess(child, layoutlibCallback, skip);
710             }
711         }
712     }
713 
714     /**
715      * If the root layout is a CoordinatorLayout with an AppBar:
716      * Set the title of the AppBar to the title of the activity context.
717      */
setActiveToolbar(View view, BridgeContext context, SessionParams params)718     private void setActiveToolbar(View view, BridgeContext context, SessionParams params) {
719         View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT);
720         if (coordinatorLayout == null) {
721             return;
722         }
723         View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT);
724         if (appBar == null) {
725             return;
726         }
727         ViewGroup collapsingToolbar =
728                 (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT);
729         if (collapsingToolbar == null) {
730             return;
731         }
732         if (!hasToolbar(collapsingToolbar)) {
733             return;
734         }
735         String title = params.getAppLabel();
736         DesignLibUtil.setTitle(collapsingToolbar, title);
737     }
738 
findChildView(View view, String[] className)739     private View findChildView(View view, String[] className) {
740         if (!(view instanceof ViewGroup)) {
741             return null;
742         }
743         ViewGroup group = (ViewGroup) view;
744         for (int i = 0; i < group.getChildCount(); i++) {
745             if (isInstanceOf(group.getChildAt(i), className)) {
746                 return group.getChildAt(i);
747             }
748         }
749         return null;
750     }
751 
hasToolbar(View collapsingToolbar)752     private boolean hasToolbar(View collapsingToolbar) {
753         if (!(collapsingToolbar instanceof ViewGroup)) {
754             return false;
755         }
756         ViewGroup group = (ViewGroup) collapsingToolbar;
757         for (int i = 0; i < group.getChildCount(); i++) {
758             if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) {
759                 return true;
760             }
761         }
762         return false;
763     }
764 
765     /**
766      * Set the scroll position on all the components with the "scrollX" and "scrollY" attribute. If
767      * the component supports nested scrolling attempt that first, then use the unconsumed scroll
768      * part to scroll the content in the component.
769      */
handleScrolling(BridgeContext context, View view)770     private static void handleScrolling(BridgeContext context, View view) {
771         int scrollPosX = context.getScrollXPos(view);
772         int scrollPosY = context.getScrollYPos(view);
773         if (scrollPosX != 0 || scrollPosY != 0) {
774             if (view.isNestedScrollingEnabled()) {
775                 int[] consumed = new int[2];
776                 int axis = scrollPosX != 0 ? View.SCROLL_AXIS_HORIZONTAL : 0;
777                 axis |= scrollPosY != 0 ? View.SCROLL_AXIS_VERTICAL : 0;
778                 if (view.startNestedScroll(axis)) {
779                     view.dispatchNestedPreScroll(scrollPosX, scrollPosY, consumed, null);
780                     view.dispatchNestedScroll(consumed[0], consumed[1], scrollPosX, scrollPosY,
781                             null);
782                     view.stopNestedScroll();
783                     scrollPosX -= consumed[0];
784                     scrollPosY -= consumed[1];
785                 }
786             }
787             if (scrollPosX != 0 || scrollPosY != 0) {
788                 view.scrollTo(scrollPosX, scrollPosY);
789             }
790         }
791 
792         if (!(view instanceof ViewGroup)) {
793             return;
794         }
795         ViewGroup group = (ViewGroup) view;
796         for (int i = 0; i < group.getChildCount(); i++) {
797             View child = group.getChildAt(i);
798             handleScrolling(context, child);
799         }
800     }
801 
802     /**
803      * Sets up a {@link TabHost} object.
804      * @param tabHost the TabHost to setup.
805      * @param layoutlibCallback The project callback object to access the project R class.
806      * @throws PostInflateException if TabHost is missing the required ids for TabHost
807      */
setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)808     private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)
809             throws PostInflateException {
810         // look for the TabWidget, and the FrameLayout. They have their own specific names
811         View v = tabHost.findViewById(android.R.id.tabs);
812 
813         if (v == null) {
814             throw new PostInflateException(
815                     "TabHost requires a TabWidget with id \"android:id/tabs\".\n");
816         }
817 
818         if (!(v instanceof TabWidget)) {
819             throw new PostInflateException(String.format(
820                     "TabHost requires a TabWidget with id \"android:id/tabs\".\n" +
821                     "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName()));
822         }
823 
824         v = tabHost.findViewById(android.R.id.tabcontent);
825 
826         if (v == null) {
827             // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty)
828             //noinspection SpellCheckingInspection
829             throw new PostInflateException(
830                     "TabHost requires a FrameLayout with id \"android:id/tabcontent\".");
831         }
832 
833         if (!(v instanceof FrameLayout)) {
834             //noinspection SpellCheckingInspection
835             throw new PostInflateException(String.format(
836                     "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" +
837                     "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName()));
838         }
839 
840         FrameLayout content = (FrameLayout)v;
841 
842         // now process the content of the frameLayout and dynamically create tabs for it.
843         final int count = content.getChildCount();
844 
845         // this must be called before addTab() so that the TabHost searches its TabWidget
846         // and FrameLayout.
847         if (isInstanceOf(tabHost, FragmentTabHostUtil.CN_FRAGMENT_TAB_HOST)) {
848             FragmentTabHostUtil.setup(tabHost, getContext());
849         } else {
850             tabHost.setup();
851         }
852 
853         if (count == 0) {
854             // Create a placeholder child to get a single tab
855             TabSpec spec = tabHost.newTabSpec("tag")
856                     .setIndicator("Tab Label", tabHost.getResources()
857                             .getDrawable(android.R.drawable.ic_menu_info_details, null))
858                     .setContent(tag -> new LinearLayout(getContext()));
859             tabHost.addTab(spec);
860         } else {
861             // for each child of the frameLayout, add a new TabSpec
862             for (int i = 0 ; i < count ; i++) {
863                 View child = content.getChildAt(i);
864                 String tabSpec = String.format("tab_spec%d", i+1);
865                 @SuppressWarnings("ConstantConditions")  // child cannot be null.
866                 int id = child.getId();
867                 @SuppressWarnings("deprecation")
868                 ResourceReference resource = layoutlibCallback.resolveResourceId(id);
869                 String name;
870                 if (resource != null) {
871                     name = resource.getName();
872                 } else {
873                     name = String.format("Tab %d", i+1); // default name if id is unresolved.
874                 }
875                 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id));
876             }
877         }
878     }
879 
880     /**
881      * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the
882      * bounds of all the views.
883      *
884      * @param view the root View
885      * @param hOffset horizontal offset for the view bounds.
886      * @param vOffset vertical offset for the view bounds.
887      * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
888      * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
889      *                       content frame.
890      *
891      * @return {@code ViewInfo} containing the bounds of the view and it children otherwise.
892      */
visit(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)893     private ViewInfo visit(View view, int hOffset, int vOffset, boolean setExtendedInfo,
894             boolean isContentFrame) {
895         ViewInfo result = createViewInfo(view, hOffset, vOffset, setExtendedInfo, isContentFrame);
896 
897         if (view instanceof ViewGroup) {
898             ViewGroup group = ((ViewGroup) view);
899             result.setChildren(visitAllChildren(group, isContentFrame ? 0 : hOffset,
900                     isContentFrame ? 0 : vOffset,
901                     setExtendedInfo, isContentFrame));
902         }
903         return result;
904     }
905 
906     /**
907      * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo}
908      * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with
909      * the children of the {@code mContentRoot}.
910      *
911      * @param viewGroup the root View
912      * @param hOffset horizontal offset from the top for the content view frame.
913      * @param vOffset vertical offset from the top for the content view frame.
914      * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object.
915      * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the
916      *                       content frame. {@code false} if the {@code ViewInfo} to be created is
917      *                       part of the system decor.
918      */
visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)919     private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset,
920             boolean setExtendedInfo, boolean isContentFrame) {
921         if (viewGroup == null) {
922             return null;
923         }
924 
925         if (!isContentFrame) {
926             vOffset += viewGroup.getTop();
927             hOffset += viewGroup.getLeft();
928         }
929 
930         int childCount = viewGroup.getChildCount();
931         if (viewGroup == mContentRoot) {
932             List<ViewInfo> childrenWithoutOffset = new ArrayList<>(childCount);
933             List<ViewInfo> childrenWithOffset = new ArrayList<>(childCount);
934             for (int i = 0; i < childCount; i++) {
935                 ViewInfo[] childViewInfo =
936                         visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset,
937                         setExtendedInfo);
938                 childrenWithoutOffset.add(childViewInfo[0]);
939                 childrenWithOffset.add(childViewInfo[1]);
940             }
941             mViewInfoList = childrenWithOffset;
942             return childrenWithoutOffset;
943         } else {
944             List<ViewInfo> children = new ArrayList<>(childCount);
945             for (int i = 0; i < childCount; i++) {
946                 children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo,
947                         isContentFrame));
948             }
949             return children;
950         }
951     }
952 
953     /**
954      * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the
955      * bounds of all the views. It returns two {@code ViewInfo} objects with the same children,
956      * one with the {@code offset} and other without the {@code offset}. The offset is needed to
957      * get the right bounds if the {@code ViewInfo} hierarchy is accessed from
958      * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the
959      * offset is not needed.
960      *
961      * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at
962      *         index 1 is with the offset.
963      */
964     @NonNull
visitContentRoot(View view, int hOffset, int vOffset, boolean setExtendedInfo)965     private ViewInfo[] visitContentRoot(View view, int hOffset, int vOffset,
966             boolean setExtendedInfo) {
967         ViewInfo[] result = new ViewInfo[2];
968         if (view == null) {
969             return result;
970         }
971 
972         result[0] = createViewInfo(view, 0, 0, setExtendedInfo, true);
973         result[1] = createViewInfo(view, hOffset, vOffset, setExtendedInfo, true);
974         if (view instanceof ViewGroup) {
975             List<ViewInfo> children =
976                     visitAllChildren((ViewGroup) view, 0, 0, setExtendedInfo, true);
977             result[0].setChildren(children);
978             result[1].setChildren(children);
979         }
980         return result;
981     }
982 
983     /**
984      * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children
985      * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not
986      * set.
987      * @param hOffset horizontal offset for the view bounds. Used only if view is part of the
988      * content frame.
989      * @param vOffset vertial an offset for the view bounds. Used only if view is part of the
990      * content frame.
991      */
createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)992     private ViewInfo createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo,
993             boolean isContentFrame) {
994         if (view == null) {
995             return null;
996         }
997 
998         ViewParent parent = view.getParent();
999         ViewInfo result;
1000         if (isContentFrame) {
1001             // Account for parent scroll values when calculating the bounding box
1002             int scrollX = parent != null ? ((View)parent).getScrollX() : 0;
1003             int scrollY = parent != null ? ((View)parent).getScrollY() : 0;
1004 
1005             // The view is part of the layout added by the user. Hence,
1006             // the ViewCookie may be obtained only through the Context.
1007             int shiftX = -scrollX + Math.round(view.getTranslationX()) + hOffset;
1008             int shiftY = -scrollY + Math.round(view.getTranslationY()) + vOffset;
1009             result = new ViewInfo(view.getClass().getName(),
1010                     getContext().getViewKey(view),
1011                     shiftX + view.getLeft(),
1012                     shiftY + view.getTop(),
1013                     shiftX + view.getRight(),
1014                     shiftY + view.getBottom(),
1015                     view, view.getLayoutParams());
1016         } else {
1017             // We are part of the system decor.
1018             SystemViewInfo r = new SystemViewInfo(view.getClass().getName(),
1019                     getViewKey(view),
1020                     view.getLeft(), view.getTop(), view.getRight(),
1021                     view.getBottom(), view, view.getLayoutParams());
1022             result = r;
1023             // We currently mark three kinds of views:
1024             // 1. Menus in the Action Bar
1025             // 2. Menus in the Overflow popup.
1026             // 3. The overflow popup button.
1027             if (view instanceof ListMenuItemView) {
1028                 // Mark 2.
1029                 // All menus in the popup are of type ListMenuItemView.
1030                 r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU);
1031             } else {
1032                 // Mark 3.
1033                 ViewGroup.LayoutParams lp = view.getLayoutParams();
1034                 if (lp instanceof ActionMenuView.LayoutParams &&
1035                         ((ActionMenuView.LayoutParams) lp).isOverflowButton) {
1036                     r.setViewType(ViewType.ACTION_BAR_OVERFLOW);
1037                 } else {
1038                     // Mark 1.
1039                     // A view is a menu in the Action Bar is it is not the overflow button and of
1040                     // its parent is of type ActionMenuView. We can also check if the view is
1041                     // instanceof ActionMenuItemView but that will fail for menus using
1042                     // actionProviderClass.
1043                     while (parent != mViewRoot && parent instanceof ViewGroup) {
1044                         if (parent instanceof ActionMenuView) {
1045                             r.setViewType(ViewType.ACTION_BAR_MENU);
1046                             break;
1047                         }
1048                         parent = parent.getParent();
1049                     }
1050                 }
1051             }
1052         }
1053 
1054         if (setExtendedInfo) {
1055             MarginLayoutParams marginParams = null;
1056             LayoutParams params = view.getLayoutParams();
1057             if (params instanceof MarginLayoutParams) {
1058                 marginParams = (MarginLayoutParams) params;
1059             }
1060             result.setExtendedInfo(view.getBaseline(),
1061                     marginParams != null ? marginParams.leftMargin : 0,
1062                     marginParams != null ? marginParams.topMargin : 0,
1063                     marginParams != null ? marginParams.rightMargin : 0,
1064                     marginParams != null ? marginParams.bottomMargin : 0);
1065         }
1066 
1067         return result;
1068     }
1069 
1070     /* (non-Javadoc)
1071      * The cookie for menu items are stored in menu item and not in the map from View stored in
1072      * BridgeContext.
1073      */
1074     @Nullable
getViewKey(View view)1075     private Object getViewKey(View view) {
1076         BridgeContext context = getContext();
1077         if (!(view instanceof MenuView.ItemView)) {
1078             return context.getViewKey(view);
1079         }
1080         MenuItemImpl menuItem;
1081         if (view instanceof ActionMenuItemView) {
1082             menuItem = ((ActionMenuItemView) view).getItemData();
1083         } else if (view instanceof ListMenuItemView) {
1084             menuItem = ((ListMenuItemView) view).getItemData();
1085         } else if (view instanceof IconMenuItemView) {
1086             menuItem = ((IconMenuItemView) view).getItemData();
1087         } else {
1088             menuItem = null;
1089         }
1090         if (menuItem instanceof BridgeMenuItemImpl) {
1091             return ((BridgeMenuItemImpl) menuItem).getViewCookie();
1092         }
1093 
1094         return null;
1095     }
1096 
invalidateRenderingSize()1097     public void invalidateRenderingSize() {
1098         mMeasuredScreenWidth = mMeasuredScreenHeight = -1;
1099     }
1100 
getImage()1101     public BufferedImage getImage() {
1102         return mImage;
1103     }
1104 
isAlphaChannelImage()1105     public boolean isAlphaChannelImage() {
1106         return mIsAlphaChannelImage;
1107     }
1108 
getViewInfos()1109     public List<ViewInfo> getViewInfos() {
1110         return mViewInfoList;
1111     }
1112 
getSystemViewInfos()1113     public List<ViewInfo> getSystemViewInfos() {
1114         return mSystemViewInfoList;
1115     }
1116 
getDefaultNamespacedProperties()1117     public Map<Object, Map<ResourceReference, ResourceValue>> getDefaultNamespacedProperties() {
1118         return getContext().getDefaultProperties();
1119     }
1120 
getDefaultStyles()1121     public Map<Object, String> getDefaultStyles() {
1122         Map<Object, String> defaultStyles = new IdentityHashMap<>();
1123         Map<Object, ResourceReference> namespacedStyles = getDefaultNamespacedStyles();
1124         for (Object key : namespacedStyles.keySet()) {
1125             ResourceReference style = namespacedStyles.get(key);
1126             defaultStyles.put(key, style.getQualifiedName());
1127         }
1128         return defaultStyles;
1129     }
1130 
getDefaultNamespacedStyles()1131     public Map<Object, ResourceReference> getDefaultNamespacedStyles() {
1132         return getContext().getDefaultNamespacedStyles();
1133     }
1134 
setScene(RenderSession session)1135     public void setScene(RenderSession session) {
1136         mScene = session;
1137     }
1138 
getSession()1139     public RenderSession getSession() {
1140         return mScene;
1141     }
1142 
dispose()1143     public void dispose() {
1144         boolean createdLooper = false;
1145         if (Looper.myLooper() == null) {
1146             // Detaching the root view from the window will try to stop any running animations.
1147             // The stop method checks that it can run in the looper so, if there is no current
1148             // looper, we create a temporary one to complete the shutdown.
1149             Bridge.prepareThread();
1150             createdLooper = true;
1151         }
1152         AttachInfo_Accessor.detachFromWindow(mViewRoot);
1153         if (mCanvas != null) {
1154             mCanvas.release();
1155             mCanvas = null;
1156         }
1157         if (mViewInfoList != null) {
1158             mViewInfoList.clear();
1159         }
1160         if (mSystemViewInfoList != null) {
1161             mSystemViewInfoList.clear();
1162         }
1163         mImage = null;
1164         mViewRoot = null;
1165         mContentRoot = null;
1166         NinePatch_Delegate.clearCache();
1167 
1168         if (createdLooper) {
1169             Choreographer_Delegate.dispose();
1170             Bridge.cleanupThread();
1171         }
1172     }
1173 }
1174