1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import com.android.ide.common.rendering.api.AdapterBinding; 20 import com.android.ide.common.rendering.api.AndroidConstants; 21 import com.android.ide.common.rendering.api.ILayoutLog; 22 import com.android.ide.common.rendering.api.LayoutlibCallback; 23 import com.android.ide.common.rendering.api.MergeCookie; 24 import com.android.ide.common.rendering.api.ResourceNamespace; 25 import com.android.ide.common.rendering.api.ResourceReference; 26 import com.android.ide.common.rendering.api.ResourceValue; 27 import com.android.layoutlib.bridge.Bridge; 28 import com.android.layoutlib.bridge.BridgeConstants; 29 import com.android.layoutlib.bridge.MockView; 30 import com.android.layoutlib.bridge.android.BridgeContext; 31 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 32 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil; 33 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; 34 import com.android.layoutlib.bridge.impl.ParserFactory; 35 import com.android.layoutlib.bridge.impl.binding.FakeAdapter; 36 import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; 37 import com.android.layoutlib.common.util.ReflectionUtils; 38 import com.android.tools.layoutlib.annotations.NotNull; 39 import com.android.tools.layoutlib.annotations.Nullable; 40 41 import org.xmlpull.v1.XmlPullParser; 42 43 import android.annotation.NonNull; 44 import android.content.Context; 45 import android.content.res.TypedArray; 46 import android.graphics.drawable.Animatable; 47 import android.graphics.drawable.Drawable; 48 import android.util.AttributeSet; 49 import android.util.Pair; 50 import android.util.ResolvingAttributeSet; 51 import android.view.View.OnAttachStateChangeListener; 52 import android.widget.AbsListView; 53 import android.widget.AbsSpinner; 54 import android.widget.AdapterView; 55 import android.widget.ExpandableListView; 56 import android.widget.ImageView; 57 import android.widget.ListView; 58 import android.widget.NumberPicker; 59 60 import java.lang.reflect.Constructor; 61 import java.lang.reflect.InvocationTargetException; 62 import java.lang.reflect.Method; 63 import java.util.HashMap; 64 import java.util.Map; 65 import java.util.function.BiFunction; 66 67 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext; 68 69 /** 70 * Custom implementation of {@link LayoutInflater} to handle custom views. 71 */ 72 public final class BridgeInflater extends LayoutInflater { 73 private static final String INFLATER_CLASS_ATTR_NAME = "viewInflaterClass"; 74 private static final ResourceReference RES_AUTO_INFLATER_CLASS_ATTR = 75 ResourceReference.attr(ResourceNamespace.RES_AUTO, INFLATER_CLASS_ATTR_NAME); 76 private static final ResourceReference LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR = 77 ResourceReference.attr(ResourceNamespace.APPCOMPAT_LEGACY, INFLATER_CLASS_ATTR_NAME); 78 private static final ResourceReference ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR = 79 ResourceReference.attr(ResourceNamespace.APPCOMPAT, INFLATER_CLASS_ATTR_NAME); 80 private static final String LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME = 81 "android.support.v7.app.AppCompatViewInflater"; 82 private static final String ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME = 83 "androidx.appcompat.app.AppCompatViewInflater"; 84 private final LayoutlibCallback mLayoutlibCallback; 85 86 private boolean mIsInMerge = false; 87 private ResourceReference mResourceReference; 88 private Map<View, String> mOpenDrawerLayouts; 89 90 // Keep in sync with the same value in LayoutInflater. 91 private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme }; 92 93 /** 94 * List of class prefixes which are tried first by default. 95 * <p/> 96 * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. 97 */ 98 private static final String[] sClassPrefixList = { 99 "android.widget.", 100 "android.webkit.", 101 "android.app." 102 }; 103 private BiFunction<String, AttributeSet, View> mCustomInflater; 104 getClassPrefixList()105 public static String[] getClassPrefixList() { 106 return sClassPrefixList; 107 } 108 BridgeInflater(LayoutInflater original, Context newContext)109 private BridgeInflater(LayoutInflater original, Context newContext) { 110 super(original, newContext); 111 newContext = getBaseContext(newContext); 112 mLayoutlibCallback = (newContext instanceof BridgeContext) ? 113 ((BridgeContext) newContext).getLayoutlibCallback() : 114 null; 115 } 116 117 /** 118 * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object. 119 * 120 * @param context The Android application context. 121 * @param layoutlibCallback the {@link LayoutlibCallback} object. 122 */ BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback)123 public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) { 124 super(context); 125 mLayoutlibCallback = layoutlibCallback; 126 mConstructorArgs[0] = context; 127 } 128 129 @Override onCreateView(String name, AttributeSet attrs)130 public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 131 View view = createViewFromCustomInflater(name, attrs); 132 133 if (view == null) { 134 try { 135 // First try to find a class using the default Android prefixes 136 for (String prefix : sClassPrefixList) { 137 try { 138 view = createView(name, prefix, attrs); 139 if (view != null) { 140 break; 141 } 142 } catch (ClassNotFoundException e) { 143 // Ignore. We'll try again using the base class below. 144 } 145 } 146 147 // Next try using the parent loader. This will most likely only work for 148 // fully-qualified class names. 149 try { 150 if (view == null) { 151 view = super.onCreateView(name, attrs); 152 } 153 } catch (ClassNotFoundException e) { 154 // Ignore. We'll try again using the custom view loader below. 155 } 156 157 // Finally try again using the custom view loader 158 if (view == null) { 159 view = loadCustomView(name, attrs); 160 } 161 } catch (InflateException e) { 162 // Don't catch the InflateException below as that results in hiding the real cause. 163 throw e; 164 } catch (Exception e) { 165 // Wrap the real exception in a ClassNotFoundException, so that the calling method 166 // can deal with it. 167 throw new ClassNotFoundException("onCreateView", e); 168 } 169 } 170 171 setupViewInContext(view, attrs); 172 173 return view; 174 } 175 176 /** 177 * Finds the createView method in the given customInflaterClass. Since createView is 178 * currently package protected, it will show in the declared class so we iterate up the 179 * hierarchy and return the first instance we find. 180 * The returned method will be accessible. 181 */ 182 @NotNull getCreateViewMethod(Class<?> customInflaterClass)183 private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException { 184 Class<?> current = customInflaterClass; 185 do { 186 try { 187 Method method = current.getDeclaredMethod("createView", View.class, String.class, 188 Context.class, AttributeSet.class, boolean.class, boolean.class, 189 boolean.class, boolean.class); 190 method.setAccessible(true); 191 return method; 192 } catch (NoSuchMethodException ignore) { 193 } 194 current = current.getSuperclass(); 195 } while (current != null && current != Object.class); 196 197 throw new NoSuchMethodException(); 198 } 199 200 /** 201 * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the 202 * class does not exist, null is returned). 203 * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate 204 * {@code android.support.v7.app.AppCompatViewInflater} 205 */ 206 @Nullable findCustomInflater(@otNull BridgeContext bc, @NotNull LayoutlibCallback layoutlibCallback)207 private static Class<?> findCustomInflater(@NotNull BridgeContext bc, 208 @NotNull LayoutlibCallback layoutlibCallback) { 209 ResourceReference attrRef; 210 if (layoutlibCallback.isResourceNamespacingRequired()) { 211 if (layoutlibCallback.hasLegacyAppCompat()) { 212 attrRef = LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR; 213 } else if (layoutlibCallback.hasAndroidXAppCompat()) { 214 attrRef = ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR; 215 } else { 216 return null; 217 } 218 } else { 219 attrRef = RES_AUTO_INFLATER_CLASS_ATTR; 220 } 221 ResourceValue value = bc.getRenderResources().findItemInTheme(attrRef); 222 String inflaterName = value != null ? value.getValue() : null; 223 224 if (inflaterName != null) { 225 try { 226 return layoutlibCallback.findClass(inflaterName); 227 } catch (ClassNotFoundException ignore) { 228 } 229 230 // viewInflaterClass was defined but we couldn't find the class. 231 } else if (bc.isAppCompatTheme()) { 232 // Older versions of AppCompat do not define the viewInflaterClass so try to get it 233 // manually. 234 try { 235 if (layoutlibCallback.hasLegacyAppCompat()) { 236 return layoutlibCallback.findClass(LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME); 237 } else if (layoutlibCallback.hasAndroidXAppCompat()) { 238 return layoutlibCallback.findClass(ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME); 239 } 240 } catch (ClassNotFoundException ignore) { 241 } 242 } 243 244 return null; 245 } 246 247 /** 248 * Checks if there is a custom inflater and, when present, tries to instantiate the view 249 * using it. 250 */ 251 @Nullable createViewFromCustomInflater(@otNull String name, @NotNull AttributeSet attrs)252 private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) { 253 if (!mLayoutlibCallback.shouldUseCustomInflater()) { 254 return null; 255 } 256 if (mCustomInflater == null) { 257 Context context = getContext(); 258 context = getBaseContext(context); 259 if (context instanceof BridgeContext bc) { 260 Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback); 261 262 if (inflaterClass != null) { 263 try { 264 Constructor<?> constructor = inflaterClass.getDeclaredConstructor(); 265 constructor.setAccessible(true); 266 Object inflater = constructor.newInstance(); 267 Method method = getCreateViewMethod(inflaterClass); 268 mCustomInflater = (viewName, attributeSet) -> { 269 try { 270 return (View) method.invoke(inflater, null, viewName, 271 mConstructorArgs[0], 272 attributeSet, 273 false, 274 false /*readAndroidTheme*/, // No need after L 275 true /*readAppTheme*/, 276 true /*wrapContext*/); 277 } catch (IllegalAccessException | InvocationTargetException e) { 278 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e, 279 null, null); 280 } 281 return null; 282 }; 283 } catch (InvocationTargetException | IllegalAccessException | 284 NoSuchMethodException | InstantiationException ignore) { 285 } 286 } 287 } 288 289 if (mCustomInflater == null) { 290 // There is no custom inflater. We'll create a nop custom inflater to avoid the 291 // penalty of trying to instantiate again 292 mCustomInflater = (s, attributeSet) -> null; 293 } 294 } 295 296 return mCustomInflater.apply(name, attrs); 297 } 298 299 @Override createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)300 public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 301 boolean ignoreThemeAttr) { 302 View view = null; 303 if (name.equals("view")) { 304 // This is usually done by the superclass but this allows us catching the error and 305 // reporting something useful. 306 name = attrs.getAttributeValue(null, "class"); 307 308 if (name == null) { 309 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "Unable to inflate view tag without " + 310 "class attribute", null, null); 311 // We weren't able to resolve the view so we just pass a mock View to be able to 312 // continue rendering. 313 view = new MockView(context, attrs); 314 ((MockView) view).setText("view"); 315 } 316 } 317 318 try { 319 if (view == null) { 320 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); 321 } 322 } catch (InflateException e) { 323 // Creation of ContextThemeWrapper code is same as in the super method. 324 // Apply a theme wrapper, if allowed and one is specified. 325 if (!ignoreThemeAttr) { 326 try (final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME)) { 327 final int themeResId = ta.getResourceId(0, 0); 328 if (themeResId != 0) { 329 context = new ContextThemeWrapper(context, themeResId); 330 } 331 } 332 } 333 if (!(e.getCause() instanceof ClassNotFoundException)) { 334 // There is some unknown inflation exception in inflating a View that was found. 335 view = new MockView(context, attrs); 336 ((MockView) view).setText(name); 337 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e, null, null); 338 } else { 339 final Object lastContext = mConstructorArgs[0]; 340 mConstructorArgs[0] = context; 341 // try to load the class from using the custom view loader 342 try { 343 view = loadCustomView(name, attrs); 344 } catch (Exception e2) { 345 // Wrap the real exception in an InflateException so that the calling 346 // method can deal with it. 347 InflateException exception = new InflateException(); 348 if (!e2.getClass().equals(ClassNotFoundException.class)) { 349 exception.initCause(e2); 350 } else { 351 exception.initCause(e); 352 } 353 throw exception; 354 } finally { 355 mConstructorArgs[0] = lastContext; 356 } 357 } 358 } 359 360 setupViewInContext(view, attrs); 361 362 return view; 363 } 364 365 @Override inflate(int resource, ViewGroup root)366 public View inflate(int resource, ViewGroup root) { 367 Context context = getContext(); 368 context = getBaseContext(context); 369 if (context instanceof BridgeContext bridgeContext) { 370 371 ResourceValue value = null; 372 373 ResourceReference layoutInfo = Bridge.resolveResourceId(resource); 374 if (layoutInfo == null) { 375 layoutInfo = mLayoutlibCallback.resolveResourceId(resource); 376 377 } 378 if (layoutInfo != null) { 379 value = bridgeContext.getRenderResources().getResolvedResource(layoutInfo); 380 } 381 382 if (value != null) { 383 String path = value.getValue(); 384 try { 385 XmlPullParser parser = ParserFactory.create(path, true); 386 if (parser == null) { 387 return null; 388 } 389 390 BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( 391 parser, bridgeContext, value.getNamespace()); 392 393 return inflate(bridgeParser, root); 394 } catch (Exception e) { 395 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 396 "Failed to parse file " + path, e, null, null); 397 398 return null; 399 } 400 } 401 } 402 return null; 403 } 404 405 /** 406 * Instantiates the given view name and returns the instance. If the view doesn't exist, a 407 * MockView or null might be returned. 408 * @param name the custom view name 409 * @param attrs the {@link AttributeSet} to be passed to the view constructor 410 * @param silent if true, errors while loading the view won't be reported and, if the view 411 * doesn't exist, null will be returned. 412 */ loadCustomView(String name, AttributeSet attrs, boolean silent)413 private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception { 414 if (mLayoutlibCallback != null) { 415 // first get the classname in case it's not the node name 416 if (name.equals("view")) { 417 name = attrs.getAttributeValue(null, "class"); 418 if (name == null) { 419 return null; 420 } 421 } 422 423 mConstructorArgs[1] = attrs; 424 425 Object customView = silent ? 426 mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs) 427 : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs); 428 429 if (customView instanceof View) { 430 return (View)customView; 431 } 432 } 433 434 return null; 435 } 436 loadCustomView(String name, AttributeSet attrs)437 private View loadCustomView(String name, AttributeSet attrs) throws Exception { 438 return loadCustomView(name, attrs, false); 439 } 440 setupViewInContext(View view, AttributeSet attrs)441 private void setupViewInContext(View view, AttributeSet attrs) { 442 Context context = getContext(); 443 context = getBaseContext(context); 444 if (!(context instanceof BridgeContext bc)) { 445 return; 446 } 447 448 // get the view key 449 Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge); 450 if (viewKey != null) { 451 bc.addViewKey(view, viewKey); 452 } 453 String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX"); 454 if (scrollPosX != null && scrollPosX.endsWith("px")) { 455 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2)); 456 bc.setScrollXPos(view, value); 457 } 458 String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); 459 if (scrollPosY != null && scrollPosY.endsWith("px")) { 460 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2)); 461 bc.setScrollYPos(view, value); 462 } 463 if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) { 464 int resourceId = 0; 465 int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI, 466 BridgeConstants.ATTR_ITEM_COUNT, -1); 467 if (attrs instanceof ResolvingAttributeSet) { 468 ResourceValue attrListItemValue = 469 ((ResolvingAttributeSet) attrs).getResolvedAttributeValue( 470 BridgeConstants.NS_TOOLS_URI, BridgeConstants.ATTR_LIST_ITEM); 471 if (attrListItemValue != null) { 472 resourceId = bc.getResourceId(attrListItemValue.asReference(), 0); 473 } 474 } 475 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue); 476 } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) { 477 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 478 BridgeConstants.ATTR_OPEN_DRAWER); 479 if (attrVal != null) { 480 getDrawerLayoutMap().put(view, attrVal); 481 } 482 } 483 else if (view instanceof NumberPicker numberPicker) { 484 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue"); 485 if (minValue != null) { 486 numberPicker.setMinValue(Integer.parseInt(minValue)); 487 } 488 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue"); 489 if (maxValue != null) { 490 numberPicker.setMaxValue(Integer.parseInt(maxValue)); 491 } 492 } 493 else if (view instanceof ImageView img) { 494 Drawable drawable = img.getDrawable(); 495 if (drawable instanceof Animatable) { 496 if (!((Animatable) drawable).isRunning()) { 497 ((Animatable) drawable).start(); 498 } 499 } 500 } 501 else if (view instanceof ViewStub) { 502 // By default, ViewStub will be set to GONE and won't be inflate. If the XML has the 503 // tools:visibility attribute we'll workaround that behavior. 504 String visibility = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 505 AndroidConstants.ATTR_VISIBILITY); 506 507 boolean isVisible = "visible".equals(visibility); 508 if (isVisible || "invisible".equals(visibility)) { 509 // We can not inflate the view until is attached to its parent so we need to delay 510 // the setVisible call until after that happens. 511 final int visibilityValue = isVisible ? View.VISIBLE : View.INVISIBLE; 512 view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 513 @Override 514 public void onViewAttachedToWindow(View v) { 515 v.removeOnAttachStateChangeListener(this); 516 view.setVisibility(visibilityValue); 517 } 518 519 @Override 520 public void onViewDetachedFromWindow(View v) {} 521 }); 522 } 523 } 524 else if (view instanceof AdapterView<?>) { 525 // We do not need data binding support for Glance ListView, the assigned adapter should 526 // handle everything itself. 527 if (isGlanceView(view)) { 528 return; 529 } 530 531 int id = view.getId(); 532 ResourceReference listRef = bc.resolveId(id); 533 534 if (listRef != null) { 535 Map<String, String> bindingAttributes = new HashMap<>(4); 536 String header = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 537 BridgeConstants.ATTR_LIST_HEADER); 538 String footer = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 539 BridgeConstants.ATTR_LIST_FOOTER); 540 String layout = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 541 BridgeConstants.ATTR_LIST_ITEM); 542 String columns = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, 543 BridgeConstants.ATTR_NUM_COLUMNS); 544 if (header != null) { 545 bindingAttributes.put(BridgeConstants.ATTR_LIST_HEADER, header); 546 } 547 if (footer != null) { 548 bindingAttributes.put(BridgeConstants.ATTR_LIST_FOOTER, footer); 549 } 550 if (layout != null) { 551 bindingAttributes.put(BridgeConstants.ATTR_LIST_ITEM, layout); 552 } 553 if (columns != null) { 554 bindingAttributes.put(BridgeConstants.ATTR_NUM_COLUMNS, columns); 555 } 556 557 AdapterBinding binding = 558 mLayoutlibCallback.getAdapterBinding(view, bindingAttributes); 559 if (binding != null) { 560 setAdapterBinding(view, bc, listRef, binding); 561 } 562 } 563 } 564 } 565 setAdapterBinding(View view, BridgeContext bc, ResourceReference listRef, AdapterBinding binding)566 private void setAdapterBinding(View view, BridgeContext bc, ResourceReference listRef, 567 AdapterBinding binding) { 568 if (view instanceof AbsListView) { 569 if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && 570 view instanceof ListView list) { 571 572 boolean skipCallbackParser = false; 573 574 int count = binding.getHeaderCount(); 575 for (int i = 0; i < count; i++) { 576 Pair<View, Boolean> pair = 577 bc.inflateView(binding.getHeaderAt(i), list, false, 578 skipCallbackParser); 579 if (pair.first != null) { 580 list.addHeaderView(pair.first); 581 } 582 583 skipCallbackParser |= pair.second; 584 } 585 586 count = binding.getFooterCount(); 587 for (int i = 0; i < count; i++) { 588 Pair<View, Boolean> pair = 589 bc.inflateView(binding.getFooterAt(i), list, false, 590 skipCallbackParser); 591 if (pair.first != null) { 592 list.addFooterView(pair.first); 593 } 594 595 skipCallbackParser |= pair.second; 596 } 597 } 598 599 if (view instanceof ExpandableListView) { 600 ((ExpandableListView) view).setAdapter( 601 new FakeExpandableAdapter(listRef, binding, mLayoutlibCallback)); 602 } else { 603 ((AbsListView) view).setAdapter( 604 new FakeAdapter(listRef, binding, mLayoutlibCallback)); 605 } 606 } else if (view instanceof AbsSpinner) { 607 ((AbsSpinner) view).setAdapter( 608 new FakeAdapter(listRef, binding, mLayoutlibCallback)); 609 } 610 } 611 isGlanceAdapter(Class<?> clazz)612 private static boolean isGlanceAdapter(Class<?> clazz) { 613 return clazz 614 .getName() 615 .equals("androidx.glance.appwidget.preview.GlanceAppWidgetViewAdapter"); 616 } 617 618 /** 619 * Return true if the View belongs to the Glance generated hierarchy (when one of the view's 620 * parents is GlanceAppWidgetViewAdapter). 621 */ isGlanceView(View view)622 private static boolean isGlanceView(View view) { 623 if (isGlanceAdapter(view.getClass())) { 624 return true; 625 } 626 ViewParent parent = view.getParent(); 627 while (parent != null) { 628 if (isGlanceAdapter(parent.getClass())) { 629 return true; 630 } 631 parent = parent.getParent(); 632 } 633 return false; 634 } 635 setIsInMerge(boolean isInMerge)636 public void setIsInMerge(boolean isInMerge) { 637 mIsInMerge = isInMerge; 638 } 639 setResourceReference(ResourceReference reference)640 public void setResourceReference(ResourceReference reference) { 641 mResourceReference = reference; 642 } 643 644 @Override cloneInContext(Context newContext)645 public LayoutInflater cloneInContext(Context newContext) { 646 return new BridgeInflater(this, newContext); 647 } 648 getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)649 /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, 650 ResourceReference resourceReference, boolean isInMerge) { 651 652 if (!(attrs instanceof BridgeXmlBlockParser parser)) { 653 return null; 654 } 655 656 // get the view key 657 Object viewKey = parser.getViewCookie(); 658 659 if (viewKey == null) { 660 int currentDepth = parser.getDepth(); 661 662 // test whether we are in an included file or in a adapter binding view. 663 BridgeXmlBlockParser previousParser = bc.getPreviousParser(); 664 if (previousParser != null) { 665 // looks like we are inside an embedded layout. 666 // only apply the cookie of the calling node (<include>) if we are at the 667 // top level of the embedded layout. If there is a merge tag, then 668 // skip it and look for the 2nd level 669 int testDepth = isInMerge ? 2 : 1; 670 if (currentDepth == testDepth) { 671 viewKey = previousParser.getViewCookie(); 672 // if we are in a merge, wrap the cookie in a MergeCookie. 673 if (viewKey != null && isInMerge) { 674 viewKey = new MergeCookie(viewKey); 675 } 676 } 677 } else if (resourceReference != null && currentDepth == 1) { 678 // else if there's a resource reference, this means we are in an adapter 679 // binding case. Set the resource ref as the view cookie only for the top 680 // level view. 681 viewKey = resourceReference; 682 } 683 } 684 685 return viewKey; 686 } 687 postInflateProcess(View view)688 public void postInflateProcess(View view) { 689 if (mOpenDrawerLayouts != null) { 690 String gravity = mOpenDrawerLayouts.get(view); 691 if (gravity != null) { 692 DrawerLayoutUtil.openDrawer(view, gravity); 693 } 694 mOpenDrawerLayouts.remove(view); 695 } 696 } 697 698 @NonNull getDrawerLayoutMap()699 private Map<View, String> getDrawerLayoutMap() { 700 if (mOpenDrawerLayouts == null) { 701 mOpenDrawerLayouts = new HashMap<>(4); 702 } 703 return mOpenDrawerLayouts; 704 } 705 onDoneInflation()706 public void onDoneInflation() { 707 if (mOpenDrawerLayouts != null) { 708 mOpenDrawerLayouts.clear(); 709 } 710 } 711 } 712