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