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 (mCustomInflater == null) { 254 Context context = getContext(); 255 context = getBaseContext(context); 256 if (context instanceof BridgeContext) { 257 BridgeContext bc = (BridgeContext) context; 258 Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback); 259 260 if (inflaterClass != null) { 261 try { 262 Constructor<?> constructor = inflaterClass.getDeclaredConstructor(); 263 constructor.setAccessible(true); 264 Object inflater = constructor.newInstance(); 265 Method method = getCreateViewMethod(inflaterClass); 266 mCustomInflater = (viewName, attributeSet) -> { 267 try { 268 return (View) method.invoke(inflater, null, viewName, 269 mConstructorArgs[0], 270 attributeSet, 271 false, 272 false /*readAndroidTheme*/, // No need after L 273 true /*readAppTheme*/, 274 true /*wrapContext*/); 275 } catch (IllegalAccessException | InvocationTargetException e) { 276 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e, 277 null, null); 278 } 279 return null; 280 }; 281 } catch (InvocationTargetException | IllegalAccessException | 282 NoSuchMethodException | InstantiationException ignore) { 283 } 284 } 285 } 286 287 if (mCustomInflater == null) { 288 // There is no custom inflater. We'll create a nop custom inflater to avoid the 289 // penalty of trying to instantiate again 290 mCustomInflater = (s, attributeSet) -> null; 291 } 292 } 293 294 return mCustomInflater.apply(name, attrs); 295 } 296 297 @Override createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)298 public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 299 boolean ignoreThemeAttr) { 300 View view = null; 301 if (name.equals("view")) { 302 // This is usually done by the superclass but this allows us catching the error and 303 // reporting something useful. 304 name = attrs.getAttributeValue(null, "class"); 305 306 if (name == null) { 307 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "Unable to inflate view tag without " + 308 "class attribute", null, null); 309 // We weren't able to resolve the view so we just pass a mock View to be able to 310 // continue rendering. 311 view = new MockView(context, attrs); 312 ((MockView) view).setText("view"); 313 } 314 } 315 316 try { 317 if (view == null) { 318 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); 319 } 320 } catch (InflateException e) { 321 // Creation of ContextThemeWrapper code is same as in the super method. 322 // Apply a theme wrapper, if allowed and one is specified. 323 if (!ignoreThemeAttr) { 324 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); 325 final int themeResId = ta.getResourceId(0, 0); 326 if (themeResId != 0) { 327 context = new ContextThemeWrapper(context, themeResId); 328 } 329 ta.recycle(); 330 } 331 if (!(e.getCause() instanceof ClassNotFoundException)) { 332 // There is some unknown inflation exception in inflating a View that was found. 333 view = new MockView(context, attrs); 334 ((MockView) view).setText(name); 335 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e, null, null); 336 } else { 337 final Object lastContext = mConstructorArgs[0]; 338 mConstructorArgs[0] = context; 339 // try to load the class from using the custom view loader 340 try { 341 view = loadCustomView(name, attrs); 342 } catch (Exception e2) { 343 // Wrap the real exception in an InflateException so that the calling 344 // method can deal with it. 345 InflateException exception = new InflateException(); 346 if (!e2.getClass().equals(ClassNotFoundException.class)) { 347 exception.initCause(e2); 348 } else { 349 exception.initCause(e); 350 } 351 throw exception; 352 } finally { 353 mConstructorArgs[0] = lastContext; 354 } 355 } 356 } 357 358 setupViewInContext(view, attrs); 359 360 return view; 361 } 362 363 @Override inflate(int resource, ViewGroup root)364 public View inflate(int resource, ViewGroup root) { 365 Context context = getContext(); 366 context = getBaseContext(context); 367 if (context instanceof BridgeContext) { 368 BridgeContext bridgeContext = (BridgeContext)context; 369 370 ResourceValue value = null; 371 372 ResourceReference layoutInfo = Bridge.resolveResourceId(resource); 373 if (layoutInfo == null) { 374 layoutInfo = mLayoutlibCallback.resolveResourceId(resource); 375 376 } 377 if (layoutInfo != null) { 378 value = bridgeContext.getRenderResources().getResolvedResource(layoutInfo); 379 } 380 381 if (value != null) { 382 String path = value.getValue(); 383 try { 384 XmlPullParser parser = ParserFactory.create(path, true); 385 if (parser == null) { 386 return null; 387 } 388 389 BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( 390 parser, bridgeContext, value.getNamespace()); 391 392 return inflate(bridgeParser, root); 393 } catch (Exception e) { 394 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 395 "Failed to parse file " + path, e, null, null); 396 397 return null; 398 } 399 } 400 } 401 return null; 402 } 403 404 /** 405 * Instantiates the given view name and returns the instance. If the view doesn't exist, a 406 * MockView or null might be returned. 407 * @param name the custom view name 408 * @param attrs the {@link AttributeSet} to be passed to the view constructor 409 * @param silent if true, errors while loading the view won't be reported and, if the view 410 * doesn't exist, null will be returned. 411 */ loadCustomView(String name, AttributeSet attrs, boolean silent)412 private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception { 413 if (mLayoutlibCallback != null) { 414 // first get the classname in case it's not the node name 415 if (name.equals("view")) { 416 name = attrs.getAttributeValue(null, "class"); 417 if (name == null) { 418 return null; 419 } 420 } 421 422 mConstructorArgs[1] = attrs; 423 424 Object customView = silent ? 425 mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs) 426 : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs); 427 428 if (customView instanceof View) { 429 return (View)customView; 430 } 431 } 432 433 return null; 434 } 435 loadCustomView(String name, AttributeSet attrs)436 private View loadCustomView(String name, AttributeSet attrs) throws Exception { 437 return loadCustomView(name, attrs, false); 438 } 439 setupViewInContext(View view, AttributeSet attrs)440 private void setupViewInContext(View view, AttributeSet attrs) { 441 Context context = getContext(); 442 context = getBaseContext(context); 443 if (!(context instanceof BridgeContext)) { 444 return; 445 } 446 447 BridgeContext bc = (BridgeContext) context; 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) { 484 NumberPicker numberPicker = (NumberPicker) view; 485 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue"); 486 if (minValue != null) { 487 numberPicker.setMinValue(Integer.parseInt(minValue)); 488 } 489 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue"); 490 if (maxValue != null) { 491 numberPicker.setMaxValue(Integer.parseInt(maxValue)); 492 } 493 } 494 else if (view instanceof ImageView) { 495 ImageView img = (ImageView) view; 496 Drawable drawable = img.getDrawable(); 497 if (drawable instanceof Animatable) { 498 if (!((Animatable) drawable).isRunning()) { 499 ((Animatable) drawable).start(); 500 } 501 } 502 } 503 else if (view instanceof ViewStub) { 504 // By default, ViewStub will be set to GONE and won't be inflate. If the XML has the 505 // tools:visibility attribute we'll workaround that behavior. 506 String visibility = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 507 AndroidConstants.ATTR_VISIBILITY); 508 509 boolean isVisible = "visible".equals(visibility); 510 if (isVisible || "invisible".equals(visibility)) { 511 // We can not inflate the view until is attached to its parent so we need to delay 512 // the setVisible call until after that happens. 513 final int visibilityValue = isVisible ? View.VISIBLE : View.INVISIBLE; 514 view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 515 @Override 516 public void onViewAttachedToWindow(View v) { 517 v.removeOnAttachStateChangeListener(this); 518 view.setVisibility(visibilityValue); 519 } 520 521 @Override 522 public void onViewDetachedFromWindow(View v) {} 523 }); 524 } 525 } 526 else if (view instanceof AdapterView<?>) { 527 // We do not need data binding support for Glance ListView, the assigned adapter should 528 // handle everything itself. 529 if (isGlanceView(view)) { 530 return; 531 } 532 533 int id = view.getId(); 534 ResourceReference listRef = bc.resolveId(id); 535 536 if (listRef != null) { 537 Map<String, String> bindingAttributes = new HashMap<>(4); 538 String header = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 539 BridgeConstants.ATTR_LIST_HEADER); 540 String footer = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 541 BridgeConstants.ATTR_LIST_FOOTER); 542 String layout = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 543 BridgeConstants.ATTR_LIST_ITEM); 544 String columns = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, 545 BridgeConstants.ATTR_NUM_COLUMNS); 546 if (header != null) { 547 bindingAttributes.put(BridgeConstants.ATTR_LIST_HEADER, header); 548 } 549 if (footer != null) { 550 bindingAttributes.put(BridgeConstants.ATTR_LIST_FOOTER, footer); 551 } 552 if (layout != null) { 553 bindingAttributes.put(BridgeConstants.ATTR_LIST_ITEM, layout); 554 } 555 if (columns != null) { 556 bindingAttributes.put(BridgeConstants.ATTR_NUM_COLUMNS, columns); 557 } 558 559 AdapterBinding binding = 560 mLayoutlibCallback.getAdapterBinding(view, bindingAttributes); 561 if (binding != null) { 562 setAdapterBinding(view, bc, listRef, binding); 563 } 564 } 565 } 566 } 567 setAdapterBinding(View view, BridgeContext bc, ResourceReference listRef, AdapterBinding binding)568 private void setAdapterBinding(View view, BridgeContext bc, ResourceReference listRef, 569 AdapterBinding binding) { 570 if (view instanceof AbsListView) { 571 if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && 572 view instanceof ListView) { 573 ListView list = (ListView) view; 574 575 boolean skipCallbackParser = false; 576 577 int count = binding.getHeaderCount(); 578 for (int i = 0; i < count; i++) { 579 Pair<View, Boolean> pair = 580 bc.inflateView(binding.getHeaderAt(i), list, false, 581 skipCallbackParser); 582 if (pair.first != null) { 583 list.addHeaderView(pair.first); 584 } 585 586 skipCallbackParser |= pair.second; 587 } 588 589 count = binding.getFooterCount(); 590 for (int i = 0; i < count; i++) { 591 Pair<View, Boolean> pair = 592 bc.inflateView(binding.getFooterAt(i), list, false, 593 skipCallbackParser); 594 if (pair.first != null) { 595 list.addFooterView(pair.first); 596 } 597 598 skipCallbackParser |= pair.second; 599 } 600 } 601 602 if (view instanceof ExpandableListView) { 603 ((ExpandableListView) view).setAdapter( 604 new FakeExpandableAdapter(listRef, binding, mLayoutlibCallback)); 605 } else { 606 ((AbsListView) view).setAdapter( 607 new FakeAdapter(listRef, binding, mLayoutlibCallback)); 608 } 609 } else if (view instanceof AbsSpinner) { 610 ((AbsSpinner) view).setAdapter( 611 new FakeAdapter(listRef, binding, mLayoutlibCallback)); 612 } 613 } 614 isGlanceAdapter(Class<?> clazz)615 private static boolean isGlanceAdapter(Class<?> clazz) { 616 return clazz 617 .getName() 618 .equals("androidx.glance.appwidget.preview.GlanceAppWidgetViewAdapter"); 619 } 620 621 /** 622 * Return true if the View belongs to the Glance generated hierarchy (when one of the view's 623 * parents is GlanceAppWidgetViewAdapter). 624 */ isGlanceView(View view)625 private static boolean isGlanceView(View view) { 626 if (isGlanceAdapter(view.getClass())) { 627 return true; 628 } 629 ViewParent parent = view.getParent(); 630 while (parent != null) { 631 if (isGlanceAdapter(parent.getClass())) { 632 return true; 633 } 634 parent = parent.getParent(); 635 } 636 return false; 637 } 638 setIsInMerge(boolean isInMerge)639 public void setIsInMerge(boolean isInMerge) { 640 mIsInMerge = isInMerge; 641 } 642 setResourceReference(ResourceReference reference)643 public void setResourceReference(ResourceReference reference) { 644 mResourceReference = reference; 645 } 646 647 @Override cloneInContext(Context newContext)648 public LayoutInflater cloneInContext(Context newContext) { 649 return new BridgeInflater(this, newContext); 650 } 651 getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)652 /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, 653 ResourceReference resourceReference, boolean isInMerge) { 654 655 if (!(attrs instanceof BridgeXmlBlockParser)) { 656 return null; 657 } 658 BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs); 659 660 // get the view key 661 Object viewKey = parser.getViewCookie(); 662 663 if (viewKey == null) { 664 int currentDepth = parser.getDepth(); 665 666 // test whether we are in an included file or in a adapter binding view. 667 BridgeXmlBlockParser previousParser = bc.getPreviousParser(); 668 if (previousParser != null) { 669 // looks like we are inside an embedded layout. 670 // only apply the cookie of the calling node (<include>) if we are at the 671 // top level of the embedded layout. If there is a merge tag, then 672 // skip it and look for the 2nd level 673 int testDepth = isInMerge ? 2 : 1; 674 if (currentDepth == testDepth) { 675 viewKey = previousParser.getViewCookie(); 676 // if we are in a merge, wrap the cookie in a MergeCookie. 677 if (viewKey != null && isInMerge) { 678 viewKey = new MergeCookie(viewKey); 679 } 680 } 681 } else if (resourceReference != null && currentDepth == 1) { 682 // else if there's a resource reference, this means we are in an adapter 683 // binding case. Set the resource ref as the view cookie only for the top 684 // level view. 685 viewKey = resourceReference; 686 } 687 } 688 689 return viewKey; 690 } 691 postInflateProcess(View view)692 public void postInflateProcess(View view) { 693 if (mOpenDrawerLayouts != null) { 694 String gravity = mOpenDrawerLayouts.get(view); 695 if (gravity != null) { 696 DrawerLayoutUtil.openDrawer(view, gravity); 697 } 698 mOpenDrawerLayouts.remove(view); 699 } 700 } 701 702 @NonNull getDrawerLayoutMap()703 private Map<View, String> getDrawerLayoutMap() { 704 if (mOpenDrawerLayouts == null) { 705 mOpenDrawerLayouts = new HashMap<>(4); 706 } 707 return mOpenDrawerLayouts; 708 } 709 onDoneInflation()710 public void onDoneInflation() { 711 if (mOpenDrawerLayouts != null) { 712 mOpenDrawerLayouts.clear(); 713 } 714 } 715 } 716