1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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.ide.eclipse.adt.internal.editors.layout; 18 19 import static com.android.SdkConstants.ANDROID_PKG_PREFIX; 20 import static com.android.SdkConstants.CALENDAR_VIEW; 21 import static com.android.SdkConstants.CLASS_VIEW; 22 import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; 23 import static com.android.SdkConstants.FQCN_GRID_VIEW; 24 import static com.android.SdkConstants.FQCN_SPINNER; 25 import static com.android.SdkConstants.GRID_VIEW; 26 import static com.android.SdkConstants.LIST_VIEW; 27 import static com.android.SdkConstants.SPINNER; 28 import static com.android.SdkConstants.VIEW_FRAGMENT; 29 import static com.android.SdkConstants.VIEW_INCLUDE; 30 31 import com.android.SdkConstants; 32 import com.android.ide.common.rendering.LayoutLibrary; 33 import com.android.ide.common.rendering.api.AdapterBinding; 34 import com.android.ide.common.rendering.api.DataBindingItem; 35 import com.android.ide.common.rendering.api.ILayoutPullParser; 36 import com.android.ide.common.rendering.api.IProjectCallback; 37 import com.android.ide.common.rendering.api.LayoutLog; 38 import com.android.ide.common.rendering.api.ResourceReference; 39 import com.android.ide.common.rendering.api.ResourceValue; 40 import com.android.ide.common.rendering.api.Result; 41 import com.android.ide.common.rendering.legacy.LegacyCallback; 42 import com.android.ide.common.resources.ResourceResolver; 43 import com.android.ide.common.xml.ManifestData; 44 import com.android.ide.eclipse.adt.AdtConstants; 45 import com.android.ide.eclipse.adt.AdtPlugin; 46 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata; 47 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderLogger; 48 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 49 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 50 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectClassLoader; 51 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 52 import com.android.resources.ResourceType; 53 import com.android.util.Pair; 54 import com.google.common.base.Charsets; 55 import com.google.common.io.Files; 56 57 import org.eclipse.core.resources.IProject; 58 import org.xmlpull.v1.XmlPullParser; 59 import org.xmlpull.v1.XmlPullParserException; 60 61 import java.io.File; 62 import java.io.FileNotFoundException; 63 import java.io.IOException; 64 import java.io.StringReader; 65 import java.lang.reflect.Constructor; 66 import java.lang.reflect.Field; 67 import java.lang.reflect.Method; 68 import java.util.HashMap; 69 import java.util.Map; 70 import java.util.Set; 71 import java.util.TreeSet; 72 73 /** 74 * Loader for Android Project class in order to use them in the layout editor. 75 * <p/>This implements {@link IProjectCallback} for the old and new API through 76 * {@link LegacyCallback} 77 */ 78 public final class ProjectCallback extends LegacyCallback { 79 private final HashMap<String, Class<?>> mLoadedClasses = new HashMap<String, Class<?>>(); 80 private final Set<String> mMissingClasses = new TreeSet<String>(); 81 private final Set<String> mBrokenClasses = new TreeSet<String>(); 82 private final IProject mProject; 83 private final ClassLoader mParentClassLoader; 84 private final ProjectResources mProjectRes; 85 private boolean mUsed = false; 86 private String mNamespace; 87 private ProjectClassLoader mLoader = null; 88 private LayoutLog mLogger; 89 private LayoutLibrary mLayoutLib; 90 91 private String mLayoutName; 92 private ILayoutPullParser mLayoutEmbeddedParser; 93 private ResourceResolver mResourceResolver; 94 95 /** 96 * Creates a new {@link ProjectCallback} to be used with the layout lib. 97 * 98 * @param layoutLib The layout library this callback is going to be invoked from 99 * @param projectRes the {@link ProjectResources} for the project. 100 * @param project the project. 101 */ ProjectCallback(LayoutLibrary layoutLib, ProjectResources projectRes, IProject project)102 public ProjectCallback(LayoutLibrary layoutLib, 103 ProjectResources projectRes, IProject project) { 104 mLayoutLib = layoutLib; 105 mParentClassLoader = layoutLib.getClassLoader(); 106 mProjectRes = projectRes; 107 mProject = project; 108 } 109 getMissingClasses()110 public Set<String> getMissingClasses() { 111 return mMissingClasses; 112 } 113 getUninstantiatableClasses()114 public Set<String> getUninstantiatableClasses() { 115 return mBrokenClasses; 116 } 117 118 /** 119 * Sets the {@link LayoutLog} logger to use for error messages during problems 120 * 121 * @param logger the new logger to use, or null to clear it out 122 */ setLogger(LayoutLog logger)123 public void setLogger(LayoutLog logger) { 124 mLogger = logger; 125 } 126 127 /** 128 * Returns the {@link LayoutLog} logger used for error messages, or null 129 * 130 * @return the logger being used, or null if no logger is in use 131 */ getLogger()132 public LayoutLog getLogger() { 133 return mLogger; 134 } 135 136 /** 137 * {@inheritDoc} 138 * 139 * This implementation goes through the output directory of the Eclipse project and loads the 140 * <code>.class</code> file directly. 141 */ 142 @Override 143 @SuppressWarnings("unchecked") loadView(String className, Class[] constructorSignature, Object[] constructorParameters)144 public Object loadView(String className, Class[] constructorSignature, 145 Object[] constructorParameters) 146 throws ClassNotFoundException, Exception { 147 mUsed = true; 148 149 if (className == null) { 150 // Just make a plain <View> if you specify <view> without a class= attribute. 151 className = CLASS_VIEW; 152 } 153 154 // look for a cached version 155 Class<?> clazz = mLoadedClasses.get(className); 156 if (clazz != null) { 157 return instantiateClass(clazz, constructorSignature, constructorParameters); 158 } 159 160 // load the class. 161 162 try { 163 if (mLoader == null) { 164 mLoader = new ProjectClassLoader(mParentClassLoader, mProject); 165 } 166 clazz = mLoader.loadClass(className); 167 } catch (Exception e) { 168 // Add the missing class to the list so that the renderer can print them later. 169 // no need to log this. 170 if (!className.equals(VIEW_FRAGMENT) && !className.equals(VIEW_INCLUDE)) { 171 mMissingClasses.add(className); 172 } 173 } 174 175 try { 176 if (clazz != null) { 177 // first try to instantiate it because adding it the list of loaded class so that 178 // we don't add broken classes. 179 Object view = instantiateClass(clazz, constructorSignature, constructorParameters); 180 mLoadedClasses.put(className, clazz); 181 182 return view; 183 } 184 } catch (Throwable e) { 185 // Find root cause to log it. 186 while (e.getCause() != null) { 187 e = e.getCause(); 188 } 189 190 AdtPlugin.log(e, "%1$s failed to instantiate.", className); //$NON-NLS-1$ 191 192 // Add the missing class to the list so that the renderer can print them later. 193 if (mLogger instanceof RenderLogger) { 194 RenderLogger renderLogger = (RenderLogger) mLogger; 195 renderLogger.recordThrowable(e); 196 197 } 198 mBrokenClasses.add(className); 199 } 200 201 // Create a mock view instead. We don't cache it in the mLoadedClasses map. 202 // If any exception is thrown, we'll return a CFN with the original class name instead. 203 try { 204 clazz = mLoader.loadClass(SdkConstants.CLASS_MOCK_VIEW); 205 Object view = instantiateClass(clazz, constructorSignature, constructorParameters); 206 207 // Set the text of the mock view to the simplified name of the custom class 208 Method m = view.getClass().getMethod("setText", 209 new Class<?>[] { CharSequence.class }); 210 String label = getShortClassName(className); 211 if (label.equals(VIEW_FRAGMENT)) { 212 label = "<fragment>\n" 213 + "Pick preview layout from the \"Fragment Layout\" context menu"; 214 } else if (label.equals(VIEW_INCLUDE)) { 215 label = "Text"; 216 } 217 218 m.invoke(view, label); 219 220 // Call MockView.setGravity(Gravity.CENTER) to get the text centered in 221 // MockViews. 222 // TODO: Do this in layoutlib's MockView class instead. 223 try { 224 // Look up android.view.Gravity#CENTER - or can we just hard-code 225 // the value (17) here? 226 Class<?> gravity = 227 Class.forName("android.view.Gravity", //$NON-NLS-1$ 228 true, view.getClass().getClassLoader()); 229 Field centerField = gravity.getField("CENTER"); //$NON-NLS-1$ 230 int center = centerField.getInt(null); 231 m = view.getClass().getMethod("setGravity", 232 new Class<?>[] { Integer.TYPE }); 233 // Center 234 //int center = (0x0001 << 4) | (0x0001 << 0); 235 m.invoke(view, Integer.valueOf(center)); 236 } catch (Exception e) { 237 // Not important to center views 238 } 239 240 return view; 241 } catch (Exception e) { 242 // We failed to create and return a mock view. 243 // Just throw back a CNF with the original class name. 244 throw new ClassNotFoundException(className, e); 245 } 246 } 247 getShortClassName(String fqcn)248 private String getShortClassName(String fqcn) { 249 // The name is typically a fully-qualified class name. Let's make it a tad shorter. 250 251 if (fqcn.startsWith("android.")) { //$NON-NLS-1$ 252 // For android classes, convert android.foo.Name to android...Name 253 int first = fqcn.indexOf('.'); 254 int last = fqcn.lastIndexOf('.'); 255 if (last > first) { 256 return fqcn.substring(0, first) + ".." + fqcn.substring(last); //$NON-NLS-1$ 257 } 258 } else { 259 // For custom non-android classes, it's best to keep the 2 first segments of 260 // the namespace, e.g. we want to get something like com.example...MyClass 261 int first = fqcn.indexOf('.'); 262 first = fqcn.indexOf('.', first + 1); 263 int last = fqcn.lastIndexOf('.'); 264 if (last > first) { 265 return fqcn.substring(0, first) + ".." + fqcn.substring(last); //$NON-NLS-1$ 266 } 267 } 268 269 return fqcn; 270 } 271 272 /** 273 * Returns the namespace for the project. The namespace contains a standard part + the 274 * application package. 275 * 276 * @return The package namespace of the project or null in case of error. 277 */ 278 @Override getNamespace()279 public String getNamespace() { 280 if (mNamespace == null) { 281 ManifestData manifestData = AndroidManifestHelper.parseForData(mProject); 282 if (manifestData != null) { 283 String javaPackage = manifestData.getPackage(); 284 mNamespace = String.format(AdtConstants.NS_CUSTOM_RESOURCES, javaPackage); 285 } 286 } 287 288 return mNamespace; 289 } 290 291 @Override resolveResourceId(int id)292 public Pair<ResourceType, String> resolveResourceId(int id) { 293 if (mProjectRes != null) { 294 return mProjectRes.resolveResourceId(id); 295 } 296 297 return null; 298 } 299 300 @Override resolveResourceId(int[] id)301 public String resolveResourceId(int[] id) { 302 if (mProjectRes != null) { 303 return mProjectRes.resolveStyleable(id); 304 } 305 306 return null; 307 } 308 309 @Override getResourceId(ResourceType type, String name)310 public Integer getResourceId(ResourceType type, String name) { 311 if (mProjectRes != null) { 312 return mProjectRes.getResourceId(type, name); 313 } 314 315 return null; 316 } 317 318 /** 319 * Returns whether the loader has received requests to load custom views. Note that 320 * the custom view loading may not actually have succeeded; this flag only records 321 * whether it was <b>requested</b>. 322 * <p/> 323 * This allows to efficiently only recreate when needed upon code change in the 324 * project. 325 * 326 * @return true if the loader has been asked to load custom views 327 */ isUsed()328 public boolean isUsed() { 329 return mUsed; 330 } 331 332 /** 333 * Instantiate a class object, using a specific constructor and parameters. 334 * @param clazz the class to instantiate 335 * @param constructorSignature the signature of the constructor to use 336 * @param constructorParameters the parameters to use in the constructor. 337 * @return A new class object, created using a specific constructor and parameters. 338 * @throws Exception 339 */ 340 @SuppressWarnings("unchecked") instantiateClass(Class<?> clazz, Class[] constructorSignature, Object[] constructorParameters)341 private Object instantiateClass(Class<?> clazz, 342 Class[] constructorSignature, 343 Object[] constructorParameters) throws Exception { 344 Constructor<?> constructor = null; 345 346 try { 347 constructor = clazz.getConstructor(constructorSignature); 348 349 } catch (NoSuchMethodException e) { 350 // Custom views can either implement a 3-parameter, 2-parameter or a 351 // 1-parameter. Let's synthetically build and try all the alternatives. 352 // That's kind of like switching to the other box. 353 // 354 // The 3-parameter constructor takes the following arguments: 355 // ...(Context context, AttributeSet attrs, int defStyle) 356 357 int n = constructorSignature.length; 358 if (n == 0) { 359 // There is no parameter-less constructor. Nobody should ask for one. 360 throw e; 361 } 362 363 for (int i = 3; i >= 1; i--) { 364 if (i == n) { 365 // Let's skip the one we know already fails 366 continue; 367 } 368 Class[] sig = new Class[i]; 369 Object[] params = new Object[i]; 370 371 int k = i; 372 if (n < k) { 373 k = n; 374 } 375 System.arraycopy(constructorSignature, 0, sig, 0, k); 376 System.arraycopy(constructorParameters, 0, params, 0, k); 377 378 for (k++; k <= i; k++) { 379 if (k == 2) { 380 // Parameter 2 is the AttributeSet 381 sig[k-1] = clazz.getClassLoader().loadClass("android.util.AttributeSet"); 382 params[k-1] = null; 383 384 } else if (k == 3) { 385 // Parameter 3 is the int defstyle 386 sig[k-1] = int.class; 387 params[k-1] = 0; 388 } 389 } 390 391 constructorSignature = sig; 392 constructorParameters = params; 393 394 try { 395 // Try again... 396 constructor = clazz.getConstructor(constructorSignature); 397 if (constructor != null) { 398 // Found a suitable constructor, now let's use it. 399 // (But let's warn the user if the simple View constructor was found 400 // since Unexpected Things may happen if the attribute set constructors 401 // are not found) 402 if (constructorSignature.length < 2 && mLogger != null) { 403 mLogger.warning("wrongconstructor", //$NON-NLS-1$ 404 String.format("Custom view %1$s is not using the 2- or 3-argument " 405 + "View constructors; XML attributes will not work", 406 clazz.getSimpleName()), null /*data*/); 407 } 408 break; 409 } 410 } catch (NoSuchMethodException e1) { 411 // pass 412 } 413 } 414 415 // If all the alternatives failed, throw the initial exception. 416 if (constructor == null) { 417 throw e; 418 } 419 } 420 421 constructor.setAccessible(true); 422 return constructor.newInstance(constructorParameters); 423 } 424 setLayoutParser(String layoutName, ILayoutPullParser layoutParser)425 public void setLayoutParser(String layoutName, ILayoutPullParser layoutParser) { 426 mLayoutName = layoutName; 427 mLayoutEmbeddedParser = layoutParser; 428 } 429 430 @Override getParser(String layoutName)431 public ILayoutPullParser getParser(String layoutName) { 432 // Try to compute the ResourceValue for this layout since layoutlib 433 // must be an older version which doesn't pass the value: 434 if (mResourceResolver != null) { 435 ResourceValue value = mResourceResolver.getProjectResource(ResourceType.LAYOUT, 436 layoutName); 437 if (value != null) { 438 return getParser(value); 439 } 440 } 441 442 return getParser(layoutName, null); 443 } 444 445 @Override getParser(ResourceValue layoutResource)446 public ILayoutPullParser getParser(ResourceValue layoutResource) { 447 return getParser(layoutResource.getName(), 448 new File(layoutResource.getValue())); 449 } 450 getParser(String layoutName, File xml)451 private ILayoutPullParser getParser(String layoutName, File xml) { 452 if (layoutName.equals(mLayoutName)) { 453 ILayoutPullParser parser = mLayoutEmbeddedParser; 454 // The parser should only be used once!! If it is included more than once, 455 // subsequent includes should just use a plain pull parser that is not tied 456 // to the XML model 457 mLayoutEmbeddedParser = null; 458 return parser; 459 } 460 461 // For included layouts, create a ContextPullParser such that we get the 462 // layout editor behavior in included layouts as well - which for example 463 // replaces <fragment> tags with <include>. 464 if (xml != null && xml.isFile()) { 465 ContextPullParser parser = new ContextPullParser(this, xml); 466 try { 467 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 468 String xmlText = Files.toString(xml, Charsets.UTF_8); 469 parser.setInput(new StringReader(xmlText)); 470 return parser; 471 } catch (XmlPullParserException e) { 472 AdtPlugin.log(e, null); 473 } catch (FileNotFoundException e) { 474 // Shouldn't happen since we check isFile() above 475 } catch (IOException e) { 476 AdtPlugin.log(e, null); 477 } 478 } 479 480 return null; 481 } 482 483 @Override getAdapterItemValue(ResourceReference adapterView, Object adapterCookie, ResourceReference itemRef, int fullPosition, int typePosition, int fullChildPosition, int typeChildPosition, ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue)484 public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie, 485 ResourceReference itemRef, 486 int fullPosition, int typePosition, int fullChildPosition, int typeChildPosition, 487 ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) { 488 489 // Special case for the palette preview 490 if (viewAttribute == ViewAttribute.TEXT 491 && adapterView.getName().startsWith("android_widget_")) { //$NON-NLS-1$ 492 String name = adapterView.getName(); 493 if (viewRef.getName().equals("text2")) { //$NON-NLS-1$ 494 return "Sub Item"; 495 } 496 if (fullPosition == 0) { 497 String viewName = name.substring("android_widget_".length()); 498 if (viewName.equals(EXPANDABLE_LIST_VIEW)) { 499 return "ExpandableList"; // ExpandableListView is too wide, character-wraps 500 } 501 return viewName; 502 } else { 503 return "Next Item"; 504 } 505 } 506 507 if (itemRef.isFramework()) { 508 // Special case for list_view_item_2 and friends 509 if (viewRef.getName().equals("text2")) { //$NON-NLS-1$ 510 return "Sub Item " + (fullPosition + 1); 511 } 512 } 513 514 if (viewAttribute == ViewAttribute.TEXT && ((String) defaultValue).length() == 0) { 515 return "Item " + (fullPosition + 1); 516 } 517 518 return null; 519 } 520 521 /** 522 * For the given class, finds and returns the nearest super class which is a ListView 523 * or an ExpandableListView or a GridView (which uses a list adapter), or returns null. 524 * 525 * @param clz the class of the view object 526 * @return the fully qualified class name of the list ancestor, or null if there 527 * is no list view ancestor 528 */ getListAdapterViewFqcn(Class<?> clz)529 public static String getListAdapterViewFqcn(Class<?> clz) { 530 String fqcn = clz.getName(); 531 if (fqcn.endsWith(LIST_VIEW)) { // including EXPANDABLE_LIST_VIEW 532 return fqcn; 533 } else if (fqcn.equals(FQCN_GRID_VIEW)) { 534 return fqcn; 535 } else if (fqcn.equals(FQCN_SPINNER)) { 536 return fqcn; 537 } else if (fqcn.startsWith(ANDROID_PKG_PREFIX)) { 538 return null; 539 } 540 Class<?> superClass = clz.getSuperclass(); 541 if (superClass != null) { 542 return getListAdapterViewFqcn(superClass); 543 } else { 544 // Should not happen; we would have encountered android.view.View first, 545 // and it should have been covered by the ANDROID_PKG_PREFIX case above. 546 return null; 547 } 548 } 549 550 /** 551 * Looks at the parent-chain of the view and if it finds a custom view, or a 552 * CalendarView, within the given distance then it returns true. A ListView within a 553 * CalendarView should not be assigned a custom list view type because it sets its own 554 * and then attempts to cast the layout to its own type which would fail if the normal 555 * default list item binding is used. 556 */ isWithinIllegalParent(Object viewObject, int depth)557 private boolean isWithinIllegalParent(Object viewObject, int depth) { 558 String fqcn = viewObject.getClass().getName(); 559 if (fqcn.endsWith(CALENDAR_VIEW) || !fqcn.startsWith(ANDROID_PKG_PREFIX)) { 560 return true; 561 } 562 563 if (depth > 0) { 564 Result result = mLayoutLib.getViewParent(viewObject); 565 if (result.isSuccess()) { 566 Object parent = result.getData(); 567 if (parent != null) { 568 return isWithinIllegalParent(parent, depth -1); 569 } 570 } 571 } 572 573 return false; 574 } 575 576 @Override getAdapterBinding(final ResourceReference adapterView, final Object adapterCookie, final Object viewObject)577 public AdapterBinding getAdapterBinding(final ResourceReference adapterView, 578 final Object adapterCookie, final Object viewObject) { 579 // Look for user-recorded preference for layout to be used for previews 580 if (adapterCookie instanceof UiViewElementNode) { 581 UiViewElementNode uiNode = (UiViewElementNode) adapterCookie; 582 AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, uiNode); 583 if (binding != null) { 584 return binding; 585 } 586 } else if (adapterCookie instanceof Map<?,?>) { 587 @SuppressWarnings("unchecked") 588 Map<String, String> map = (Map<String, String>) adapterCookie; 589 AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, map); 590 if (binding != null) { 591 return binding; 592 } 593 } 594 595 if (viewObject == null) { 596 return null; 597 } 598 599 // Is this a ListView or ExpandableListView? If so, return its fully qualified 600 // class name, otherwise return null. This is used to filter out other types 601 // of AdapterViews (such as Spinners) where we don't want to use the list item 602 // binding. 603 String listFqcn = getListAdapterViewFqcn(viewObject.getClass()); 604 if (listFqcn == null) { 605 return null; 606 } 607 608 // Is this ListView nested within an "illegal" container, such as a CalendarView? 609 // If so, don't change the bindings below. Some views, such as CalendarView, and 610 // potentially some custom views, might be doing specific things with the ListView 611 // that could break if we add our own list binding, so for these leave the list 612 // alone. 613 if (isWithinIllegalParent(viewObject, 2)) { 614 return null; 615 } 616 617 int count = listFqcn.endsWith(GRID_VIEW) ? 24 : 12; 618 AdapterBinding binding = new AdapterBinding(count); 619 if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) { 620 binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM, 621 true /* isFramework */, 1)); 622 } else if (listFqcn.equals(SPINNER)) { 623 binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM, 624 true /* isFramework */, 1)); 625 } else { 626 binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_LIST_ITEM, 627 true /* isFramework */, 1)); 628 } 629 630 return binding; 631 } 632 633 /** 634 * Sets the {@link ResourceResolver} to be used when looking up resources 635 * 636 * @param resolver the resolver to use 637 */ setResourceResolver(ResourceResolver resolver)638 public void setResourceResolver(ResourceResolver resolver) { 639 mResourceResolver = resolver; 640 } 641 } 642