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