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.LayoutLog; 20 import com.android.ide.common.rendering.api.LayoutlibCallback; 21 import com.android.ide.common.rendering.api.MergeCookie; 22 import com.android.ide.common.rendering.api.ResourceReference; 23 import com.android.ide.common.rendering.api.ResourceValue; 24 import com.android.layoutlib.bridge.Bridge; 25 import com.android.layoutlib.bridge.BridgeConstants; 26 import com.android.layoutlib.bridge.MockView; 27 import com.android.layoutlib.bridge.android.BridgeContext; 28 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 29 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil; 30 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; 31 import com.android.layoutlib.bridge.impl.ParserFactory; 32 import com.android.layoutlib.bridge.util.ReflectionUtils; 33 import com.android.resources.ResourceType; 34 import com.android.util.Pair; 35 36 import org.xmlpull.v1.XmlPullParser; 37 38 import android.annotation.NonNull; 39 import android.content.Context; 40 import android.content.res.TypedArray; 41 import android.graphics.drawable.Animatable; 42 import android.graphics.drawable.Drawable; 43 import android.util.AttributeSet; 44 import android.widget.ImageView; 45 import android.widget.NumberPicker; 46 47 import java.io.File; 48 import java.util.Arrays; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.Map; 53 import java.util.Set; 54 55 import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; 56 import static com.android.SdkConstants.BUTTON; 57 import static com.android.SdkConstants.CHECKED_TEXT_VIEW; 58 import static com.android.SdkConstants.CHECK_BOX; 59 import static com.android.SdkConstants.EDIT_TEXT; 60 import static com.android.SdkConstants.IMAGE_BUTTON; 61 import static com.android.SdkConstants.IMAGE_VIEW; 62 import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; 63 import static com.android.SdkConstants.RADIO_BUTTON; 64 import static com.android.SdkConstants.SEEK_BAR; 65 import static com.android.SdkConstants.SPINNER; 66 import static com.android.SdkConstants.TEXT_VIEW; 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 74 private final LayoutlibCallback mLayoutlibCallback; 75 /** 76 * If true, the inflater will try to replace the framework widgets with the AppCompat versions. 77 * Ideally, this should be based on the activity being an AppCompat activity but since that is 78 * not trivial to check from layoutlib, we currently base the decision on the current theme 79 * being an AppCompat theme. 80 */ 81 private boolean mLoadAppCompatViews; 82 /** 83 * This set contains the framework views that have an AppCompat version but failed to load. 84 * This might happen because not all widgets are contained in all versions of the support 85 * library. 86 * This will help us to avoid trying to load the AppCompat version multiple times if it 87 * doesn't exist. 88 */ 89 private Set<String> mFailedAppCompatViews = new HashSet<>(); 90 private boolean mIsInMerge = false; 91 private ResourceReference mResourceReference; 92 private Map<View, String> mOpenDrawerLayouts; 93 94 // Keep in sync with the same value in LayoutInflater. 95 private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme }; 96 97 private static final String APPCOMPAT_WIDGET_PREFIX = "android.support.v7.widget.AppCompat"; 98 /** List of platform widgets that have an AppCompat version */ 99 private static final Set<String> APPCOMPAT_VIEWS = Collections.unmodifiableSet( 100 new HashSet<>( 101 Arrays.asList(TEXT_VIEW, IMAGE_VIEW, BUTTON, EDIT_TEXT, SPINNER, 102 IMAGE_BUTTON, CHECK_BOX, RADIO_BUTTON, CHECKED_TEXT_VIEW, 103 AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW, "RatingBar", 104 SEEK_BAR))); 105 106 /** 107 * List of class prefixes which are tried first by default. 108 * <p/> 109 * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. 110 */ 111 private static final String[] sClassPrefixList = { 112 "android.widget.", 113 "android.webkit.", 114 "android.app." 115 }; 116 getClassPrefixList()117 public static String[] getClassPrefixList() { 118 return sClassPrefixList; 119 } 120 BridgeInflater(LayoutInflater original, Context newContext)121 private BridgeInflater(LayoutInflater original, Context newContext) { 122 super(original, newContext); 123 newContext = getBaseContext(newContext); 124 if (newContext instanceof BridgeContext) { 125 mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback(); 126 mLoadAppCompatViews = ((BridgeContext) newContext).isAppCompatTheme(); 127 } else { 128 mLayoutlibCallback = null; 129 mLoadAppCompatViews = false; 130 } 131 } 132 133 /** 134 * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object. 135 * 136 * @param context The Android application context. 137 * @param layoutlibCallback the {@link LayoutlibCallback} object. 138 */ BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback)139 public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) { 140 super(context); 141 mLayoutlibCallback = layoutlibCallback; 142 mConstructorArgs[0] = context; 143 mLoadAppCompatViews = context.isAppCompatTheme(); 144 } 145 146 @Override onCreateView(String name, AttributeSet attrs)147 public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 148 View view = null; 149 150 try { 151 if (mLoadAppCompatViews 152 && APPCOMPAT_VIEWS.contains(name) 153 && !mFailedAppCompatViews.contains(name)) { 154 // We are using an AppCompat theme so try to load the appcompat views 155 view = loadCustomView(APPCOMPAT_WIDGET_PREFIX + name, attrs, true); 156 157 if (view == null) { 158 mFailedAppCompatViews.add(name); // Do not try this one anymore 159 } 160 } 161 162 if (view == null) { 163 // First try to find a class using the default Android prefixes 164 for (String prefix : sClassPrefixList) { 165 try { 166 view = createView(name, prefix, attrs); 167 if (view != null) { 168 break; 169 } 170 } catch (ClassNotFoundException e) { 171 // Ignore. We'll try again using the base class below. 172 } 173 } 174 175 // Next try using the parent loader. This will most likely only work for 176 // fully-qualified class names. 177 try { 178 if (view == null) { 179 view = super.onCreateView(name, attrs); 180 } 181 } catch (ClassNotFoundException e) { 182 // Ignore. We'll try again using the custom view loader below. 183 } 184 } 185 186 // Finally try again using the custom view loader 187 if (view == null) { 188 view = loadCustomView(name, attrs); 189 } 190 } catch (InflateException e) { 191 // Don't catch the InflateException below as that results in hiding the real cause. 192 throw e; 193 } catch (Exception e) { 194 // Wrap the real exception in a ClassNotFoundException, so that the calling method 195 // can deal with it. 196 throw new ClassNotFoundException("onCreateView", e); 197 } 198 199 setupViewInContext(view, attrs); 200 201 return view; 202 } 203 204 @Override createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)205 public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 206 boolean ignoreThemeAttr) { 207 View view = null; 208 if (name.equals("view")) { 209 // This is usually done by the superclass but this allows us catching the error and 210 // reporting something useful. 211 name = attrs.getAttributeValue(null, "class"); 212 213 if (name == null) { 214 Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " + 215 "class attribute", null); 216 // We weren't able to resolve the view so we just pass a mock View to be able to 217 // continue rendering. 218 view = new MockView(context, attrs); 219 ((MockView) view).setText("view"); 220 } 221 } 222 223 try { 224 if (view == null) { 225 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); 226 } 227 } catch (InflateException e) { 228 // Creation of ContextThemeWrapper code is same as in the super method. 229 // Apply a theme wrapper, if allowed and one is specified. 230 if (!ignoreThemeAttr) { 231 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); 232 final int themeResId = ta.getResourceId(0, 0); 233 if (themeResId != 0) { 234 context = new ContextThemeWrapper(context, themeResId); 235 } 236 ta.recycle(); 237 } 238 if (!(e.getCause() instanceof ClassNotFoundException)) { 239 // There is some unknown inflation exception in inflating a View that was found. 240 view = new MockView(context, attrs); 241 ((MockView) view).setText(name); 242 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null); 243 } else { 244 final Object lastContext = mConstructorArgs[0]; 245 mConstructorArgs[0] = context; 246 // try to load the class from using the custom view loader 247 try { 248 view = loadCustomView(name, attrs); 249 } catch (Exception e2) { 250 // Wrap the real exception in an InflateException so that the calling 251 // method can deal with it. 252 InflateException exception = new InflateException(); 253 if (!e2.getClass().equals(ClassNotFoundException.class)) { 254 exception.initCause(e2); 255 } else { 256 exception.initCause(e); 257 } 258 throw exception; 259 } finally { 260 mConstructorArgs[0] = lastContext; 261 } 262 } 263 } 264 265 setupViewInContext(view, attrs); 266 267 return view; 268 } 269 270 @Override inflate(int resource, ViewGroup root)271 public View inflate(int resource, ViewGroup root) { 272 Context context = getContext(); 273 context = getBaseContext(context); 274 if (context instanceof BridgeContext) { 275 BridgeContext bridgeContext = (BridgeContext)context; 276 277 ResourceValue value = null; 278 279 @SuppressWarnings("deprecation") 280 Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource); 281 if (layoutInfo != null) { 282 value = bridgeContext.getRenderResources().getFrameworkResource( 283 ResourceType.LAYOUT, layoutInfo.getSecond()); 284 } else { 285 layoutInfo = mLayoutlibCallback.resolveResourceId(resource); 286 287 if (layoutInfo != null) { 288 value = bridgeContext.getRenderResources().getProjectResource( 289 ResourceType.LAYOUT, layoutInfo.getSecond()); 290 } 291 } 292 293 if (value != null) { 294 File f = new File(value.getValue()); 295 if (f.isFile()) { 296 try { 297 XmlPullParser parser = ParserFactory.create(f, true); 298 299 BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( 300 parser, bridgeContext, value.isFramework()); 301 302 return inflate(bridgeParser, root); 303 } catch (Exception e) { 304 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 305 "Failed to parse file " + f.getAbsolutePath(), e, null); 306 307 return null; 308 } 309 } 310 } 311 } 312 return null; 313 } 314 315 /** 316 * Instantiates the given view name and returns the instance. If the view doesn't exist, a 317 * MockView or null might be returned. 318 * @param name the custom view name 319 * @param attrs the {@link AttributeSet} to be passed to the view constructor 320 * @param silent if true, errors while loading the view won't be reported and, if the view 321 * doesn't exist, null will be returned. 322 */ loadCustomView(String name, AttributeSet attrs, boolean silent)323 private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception { 324 if (mLayoutlibCallback != null) { 325 // first get the classname in case it's not the node name 326 if (name.equals("view")) { 327 name = attrs.getAttributeValue(null, "class"); 328 if (name == null) { 329 return null; 330 } 331 } 332 333 mConstructorArgs[1] = attrs; 334 335 Object customView = silent ? 336 mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs) 337 : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs); 338 339 if (customView instanceof View) { 340 return (View)customView; 341 } 342 } 343 344 return null; 345 } 346 loadCustomView(String name, AttributeSet attrs)347 private View loadCustomView(String name, AttributeSet attrs) throws Exception { 348 return loadCustomView(name, attrs, false); 349 } 350 setupViewInContext(View view, AttributeSet attrs)351 private void setupViewInContext(View view, AttributeSet attrs) { 352 Context context = getContext(); 353 context = getBaseContext(context); 354 if (context instanceof BridgeContext) { 355 BridgeContext bc = (BridgeContext) context; 356 // get the view key 357 Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge); 358 if (viewKey != null) { 359 bc.addViewKey(view, viewKey); 360 } 361 String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX"); 362 if (scrollPosX != null && scrollPosX.endsWith("px")) { 363 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2)); 364 bc.setScrollXPos(view, value); 365 } 366 String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); 367 if (scrollPosY != null && scrollPosY.endsWith("px")) { 368 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2)); 369 bc.setScrollYPos(view, value); 370 } 371 if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) { 372 Integer resourceId = null; 373 String attrListItemValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 374 BridgeConstants.ATTR_LIST_ITEM); 375 int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI, 376 BridgeConstants.ATTR_ITEM_COUNT, -1); 377 if (attrListItemValue != null && !attrListItemValue.isEmpty()) { 378 ResourceValue resValue = bc.getRenderResources().findResValue(attrListItemValue, false); 379 if (resValue.isFramework()) { 380 resourceId = Bridge.getResourceId(resValue.getResourceType(), 381 resValue.getName()); 382 } else { 383 resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(), 384 resValue.getName()); 385 } 386 } 387 if (resourceId == null) { 388 resourceId = 0; 389 } 390 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue); 391 } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) { 392 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 393 BridgeConstants.ATTR_OPEN_DRAWER); 394 if (attrVal != null) { 395 getDrawerLayoutMap().put(view, attrVal); 396 } 397 } 398 else if (view instanceof NumberPicker) { 399 NumberPicker numberPicker = (NumberPicker) view; 400 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue"); 401 if (minValue != null) { 402 numberPicker.setMinValue(Integer.parseInt(minValue)); 403 } 404 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue"); 405 if (maxValue != null) { 406 numberPicker.setMaxValue(Integer.parseInt(maxValue)); 407 } 408 } 409 else if (view instanceof ImageView) { 410 ImageView img = (ImageView) view; 411 Drawable drawable = img.getDrawable(); 412 if (drawable instanceof Animatable) { 413 if (!((Animatable) drawable).isRunning()) { 414 ((Animatable) drawable).start(); 415 } 416 } 417 } 418 419 } 420 } 421 setIsInMerge(boolean isInMerge)422 public void setIsInMerge(boolean isInMerge) { 423 mIsInMerge = isInMerge; 424 } 425 setResourceReference(ResourceReference reference)426 public void setResourceReference(ResourceReference reference) { 427 mResourceReference = reference; 428 } 429 430 @Override cloneInContext(Context newContext)431 public LayoutInflater cloneInContext(Context newContext) { 432 return new BridgeInflater(this, newContext); 433 } 434 getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)435 /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, 436 ResourceReference resourceReference, boolean isInMerge) { 437 438 if (!(attrs instanceof BridgeXmlBlockParser)) { 439 return null; 440 } 441 BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs); 442 443 // get the view key 444 Object viewKey = parser.getViewCookie(); 445 446 if (viewKey == null) { 447 int currentDepth = parser.getDepth(); 448 449 // test whether we are in an included file or in a adapter binding view. 450 BridgeXmlBlockParser previousParser = bc.getPreviousParser(); 451 if (previousParser != null) { 452 // looks like we are inside an embedded layout. 453 // only apply the cookie of the calling node (<include>) if we are at the 454 // top level of the embedded layout. If there is a merge tag, then 455 // skip it and look for the 2nd level 456 int testDepth = isInMerge ? 2 : 1; 457 if (currentDepth == testDepth) { 458 viewKey = previousParser.getViewCookie(); 459 // if we are in a merge, wrap the cookie in a MergeCookie. 460 if (viewKey != null && isInMerge) { 461 viewKey = new MergeCookie(viewKey); 462 } 463 } 464 } else if (resourceReference != null && currentDepth == 1) { 465 // else if there's a resource reference, this means we are in an adapter 466 // binding case. Set the resource ref as the view cookie only for the top 467 // level view. 468 viewKey = resourceReference; 469 } 470 } 471 472 return viewKey; 473 } 474 postInflateProcess(View view)475 public void postInflateProcess(View view) { 476 if (mOpenDrawerLayouts != null) { 477 String gravity = mOpenDrawerLayouts.get(view); 478 if (gravity != null) { 479 DrawerLayoutUtil.openDrawer(view, gravity); 480 } 481 mOpenDrawerLayouts.remove(view); 482 } 483 } 484 485 @NonNull getDrawerLayoutMap()486 private Map<View, String> getDrawerLayoutMap() { 487 if (mOpenDrawerLayouts == null) { 488 mOpenDrawerLayouts = new HashMap<View, String>(4); 489 } 490 return mOpenDrawerLayouts; 491 } 492 onDoneInflation()493 public void onDoneInflation() { 494 if (mOpenDrawerLayouts != null) { 495 mOpenDrawerLayouts.clear(); 496 } 497 } 498 } 499