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