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