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.util.AttributeSet; 42 43 import java.io.File; 44 import java.util.HashMap; 45 import java.util.Map; 46 47 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext; 48 49 /** 50 * Custom implementation of {@link LayoutInflater} to handle custom views. 51 */ 52 public final class BridgeInflater extends LayoutInflater { 53 54 private final LayoutlibCallback mLayoutlibCallback; 55 private boolean mIsInMerge = false; 56 private ResourceReference mResourceReference; 57 private Map<View, String> mOpenDrawerLayouts; 58 59 // Keep in sync with the same value in LayoutInflater. 60 private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme }; 61 62 /** 63 * List of class prefixes which are tried first by default. 64 * <p/> 65 * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. 66 */ 67 private static final String[] sClassPrefixList = { 68 "android.widget.", 69 "android.webkit.", 70 "android.app." 71 }; 72 getClassPrefixList()73 public static String[] getClassPrefixList() { 74 return sClassPrefixList; 75 } 76 BridgeInflater(LayoutInflater original, Context newContext)77 protected BridgeInflater(LayoutInflater original, Context newContext) { 78 super(original, newContext); 79 newContext = getBaseContext(newContext); 80 if (newContext instanceof BridgeContext) { 81 mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback(); 82 } else { 83 mLayoutlibCallback = null; 84 } 85 } 86 87 /** 88 * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object. 89 * 90 * @param context The Android application context. 91 * @param layoutlibCallback the {@link LayoutlibCallback} object. 92 */ BridgeInflater(Context context, LayoutlibCallback layoutlibCallback)93 public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) { 94 super(context); 95 mLayoutlibCallback = layoutlibCallback; 96 mConstructorArgs[0] = context; 97 } 98 99 @Override onCreateView(String name, AttributeSet attrs)100 public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 101 View view = null; 102 103 try { 104 // First try to find a class using the default Android prefixes 105 for (String prefix : sClassPrefixList) { 106 try { 107 view = createView(name, prefix, attrs); 108 if (view != null) { 109 break; 110 } 111 } catch (ClassNotFoundException e) { 112 // Ignore. We'll try again using the base class below. 113 } 114 } 115 116 // Next try using the parent loader. This will most likely only work for 117 // fully-qualified class names. 118 try { 119 if (view == null) { 120 view = super.onCreateView(name, attrs); 121 } 122 } catch (ClassNotFoundException e) { 123 // Ignore. We'll try again using the custom view loader below. 124 } 125 126 // Finally try again using the custom view loader 127 if (view == null) { 128 view = loadCustomView(name, attrs); 129 } 130 } catch (InflateException e) { 131 // Don't catch the InflateException below as that results in hiding the real cause. 132 throw e; 133 } catch (Exception e) { 134 // Wrap the real exception in a ClassNotFoundException, so that the calling method 135 // can deal with it. 136 throw new ClassNotFoundException("onCreateView", e); 137 } 138 139 setupViewInContext(view, attrs); 140 141 return view; 142 } 143 144 @Override createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)145 public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 146 boolean ignoreThemeAttr) { 147 View view; 148 try { 149 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); 150 } catch (InflateException e) { 151 // Creation of ContextThemeWrapper code is same as in the super method. 152 // Apply a theme wrapper, if allowed and one is specified. 153 if (!ignoreThemeAttr) { 154 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); 155 final int themeResId = ta.getResourceId(0, 0); 156 if (themeResId != 0) { 157 context = new ContextThemeWrapper(context, themeResId); 158 } 159 ta.recycle(); 160 } 161 if (!(e.getCause() instanceof ClassNotFoundException)) { 162 // There is some unknown inflation exception in inflating a View that was found. 163 view = new MockView(context, attrs); 164 ((MockView) view).setText(name); 165 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null); 166 } else { 167 final Object lastContext = mConstructorArgs[0]; 168 mConstructorArgs[0] = context; 169 // try to load the class from using the custom view loader 170 try { 171 view = loadCustomView(name, attrs); 172 } catch (Exception e2) { 173 // Wrap the real exception in an InflateException so that the calling 174 // method can deal with it. 175 InflateException exception = new InflateException(); 176 if (!e2.getClass().equals(ClassNotFoundException.class)) { 177 exception.initCause(e2); 178 } else { 179 exception.initCause(e); 180 } 181 throw exception; 182 } finally { 183 mConstructorArgs[0] = lastContext; 184 } 185 } 186 } 187 188 setupViewInContext(view, attrs); 189 190 return view; 191 } 192 193 @Override inflate(int resource, ViewGroup root)194 public View inflate(int resource, ViewGroup root) { 195 Context context = getContext(); 196 context = getBaseContext(context); 197 if (context instanceof BridgeContext) { 198 BridgeContext bridgeContext = (BridgeContext)context; 199 200 ResourceValue value = null; 201 202 @SuppressWarnings("deprecation") 203 Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource); 204 if (layoutInfo != null) { 205 value = bridgeContext.getRenderResources().getFrameworkResource( 206 ResourceType.LAYOUT, layoutInfo.getSecond()); 207 } else { 208 layoutInfo = mLayoutlibCallback.resolveResourceId(resource); 209 210 if (layoutInfo != null) { 211 value = bridgeContext.getRenderResources().getProjectResource( 212 ResourceType.LAYOUT, layoutInfo.getSecond()); 213 } 214 } 215 216 if (value != null) { 217 File f = new File(value.getValue()); 218 if (f.isFile()) { 219 try { 220 XmlPullParser parser = ParserFactory.create(f, true); 221 222 BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( 223 parser, bridgeContext, value.isFramework()); 224 225 return inflate(bridgeParser, root); 226 } catch (Exception e) { 227 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 228 "Failed to parse file " + f.getAbsolutePath(), e, null); 229 230 return null; 231 } 232 } 233 } 234 } 235 return null; 236 } 237 loadCustomView(String name, AttributeSet attrs)238 private View loadCustomView(String name, AttributeSet attrs) throws Exception { 239 if (mLayoutlibCallback != null) { 240 // first get the classname in case it's not the node name 241 if (name.equals("view")) { 242 name = attrs.getAttributeValue(null, "class"); 243 } 244 245 mConstructorArgs[1] = attrs; 246 247 Object customView = mLayoutlibCallback.loadView(name, mConstructorSignature, 248 mConstructorArgs); 249 250 if (customView instanceof View) { 251 return (View)customView; 252 } 253 } 254 255 return null; 256 } 257 setupViewInContext(View view, AttributeSet attrs)258 private void setupViewInContext(View view, AttributeSet attrs) { 259 Context context = getContext(); 260 context = getBaseContext(context); 261 if (context instanceof BridgeContext) { 262 BridgeContext bc = (BridgeContext) context; 263 // get the view key 264 Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge); 265 if (viewKey != null) { 266 bc.addViewKey(view, viewKey); 267 } 268 String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX"); 269 if (scrollPosX != null && scrollPosX.endsWith("px")) { 270 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2)); 271 bc.setScrollXPos(view, value); 272 } 273 String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); 274 if (scrollPosY != null && scrollPosY.endsWith("px")) { 275 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2)); 276 bc.setScrollYPos(view, value); 277 } 278 if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) { 279 Integer resourceId = null; 280 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 281 BridgeConstants.ATTR_LIST_ITEM); 282 if (attrVal != null && !attrVal.isEmpty()) { 283 ResourceValue resValue = bc.getRenderResources().findResValue(attrVal, false); 284 if (resValue.isFramework()) { 285 resourceId = Bridge.getResourceId(resValue.getResourceType(), 286 resValue.getName()); 287 } else { 288 resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(), 289 resValue.getName()); 290 } 291 } 292 if (resourceId == null) { 293 resourceId = 0; 294 } 295 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId); 296 } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) { 297 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 298 BridgeConstants.ATTR_OPEN_DRAWER); 299 if (attrVal != null) { 300 getDrawerLayoutMap().put(view, attrVal); 301 } 302 } 303 304 } 305 } 306 setIsInMerge(boolean isInMerge)307 public void setIsInMerge(boolean isInMerge) { 308 mIsInMerge = isInMerge; 309 } 310 setResourceReference(ResourceReference reference)311 public void setResourceReference(ResourceReference reference) { 312 mResourceReference = reference; 313 } 314 315 @Override cloneInContext(Context newContext)316 public LayoutInflater cloneInContext(Context newContext) { 317 return new BridgeInflater(this, newContext); 318 } 319 getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)320 /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, 321 ResourceReference resourceReference, boolean isInMerge) { 322 323 if (!(attrs instanceof BridgeXmlBlockParser)) { 324 return null; 325 } 326 BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs); 327 328 // get the view key 329 Object viewKey = parser.getViewCookie(); 330 331 if (viewKey == null) { 332 int currentDepth = parser.getDepth(); 333 334 // test whether we are in an included file or in a adapter binding view. 335 BridgeXmlBlockParser previousParser = bc.getPreviousParser(); 336 if (previousParser != null) { 337 // looks like we are inside an embedded layout. 338 // only apply the cookie of the calling node (<include>) if we are at the 339 // top level of the embedded layout. If there is a merge tag, then 340 // skip it and look for the 2nd level 341 int testDepth = isInMerge ? 2 : 1; 342 if (currentDepth == testDepth) { 343 viewKey = previousParser.getViewCookie(); 344 // if we are in a merge, wrap the cookie in a MergeCookie. 345 if (viewKey != null && isInMerge) { 346 viewKey = new MergeCookie(viewKey); 347 } 348 } 349 } else if (resourceReference != null && currentDepth == 1) { 350 // else if there's a resource reference, this means we are in an adapter 351 // binding case. Set the resource ref as the view cookie only for the top 352 // level view. 353 viewKey = resourceReference; 354 } 355 } 356 357 return viewKey; 358 } 359 postInflateProcess(View view)360 public void postInflateProcess(View view) { 361 if (mOpenDrawerLayouts != null) { 362 String gravity = mOpenDrawerLayouts.get(view); 363 if (gravity != null) { 364 DrawerLayoutUtil.openDrawer(view, gravity); 365 } 366 mOpenDrawerLayouts.remove(view); 367 } 368 } 369 370 @NonNull getDrawerLayoutMap()371 private Map<View, String> getDrawerLayoutMap() { 372 if (mOpenDrawerLayouts == null) { 373 mOpenDrawerLayouts = new HashMap<View, String>(4); 374 } 375 return mOpenDrawerLayouts; 376 } 377 onDoneInflation()378 public void onDoneInflation() { 379 if (mOpenDrawerLayouts != null) { 380 mOpenDrawerLayouts.clear(); 381 } 382 } 383 } 384