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 */ 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 110 public Set<String> getMissingClasses() { 111 return mMissingClasses; 112 } 113 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 */ 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 */ 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") 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 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 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 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 301 public String resolveResourceId(int[] id) { 302 if (mProjectRes != null) { 303 return mProjectRes.resolveStyleable(id); 304 } 305 306 return null; 307 } 308 309 @Override 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 */ 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") 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 425 public void setLayoutParser(String layoutName, ILayoutPullParser layoutParser) { 426 mLayoutName = layoutName; 427 mLayoutEmbeddedParser = layoutParser; 428 } 429 430 @Override 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 446 public ILayoutPullParser getParser(ResourceValue layoutResource) { 447 return getParser(layoutResource.getName(), 448 new File(layoutResource.getValue())); 449 } 450 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 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 */ 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 */ 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 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 */ 638 public void setResourceResolver(ResourceResolver resolver) { 639 mResourceResolver = resolver; 640 } 641 } 642