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