1 /* 2 * Copyright (C) 2011 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.manifest; 18 19 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 20 import static com.android.SdkConstants.CLASS_ACTIVITY; 21 import static com.android.SdkConstants.NS_RESOURCES; 22 import static com.android.xml.AndroidManifest.ATTRIBUTE_ICON; 23 import static com.android.xml.AndroidManifest.ATTRIBUTE_LABEL; 24 import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; 25 import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; 26 import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; 27 import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME; 28 import static com.android.xml.AndroidManifest.ATTRIBUTE_SUPPORTS_RTL; 29 import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; 30 import static com.android.xml.AndroidManifest.ATTRIBUTE_THEME; 31 import static com.android.xml.AndroidManifest.ATTRIBUTE_UI_OPTIONS; 32 import static com.android.xml.AndroidManifest.ATTRIBUTE_VALUE; 33 import static com.android.xml.AndroidManifest.NODE_ACTIVITY; 34 import static com.android.xml.AndroidManifest.NODE_METADATA; 35 import static com.android.xml.AndroidManifest.NODE_USES_SDK; 36 import static com.android.xml.AndroidManifest.VALUE_PARENT_ACTIVITY; 37 import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES; 38 39 import com.android.SdkConstants; 40 import com.android.annotations.NonNull; 41 import com.android.annotations.Nullable; 42 import com.android.ide.eclipse.adt.AdtPlugin; 43 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 44 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 45 import com.android.ide.eclipse.adt.io.IFolderWrapper; 46 import com.android.io.IAbstractFile; 47 import com.android.io.StreamException; 48 import com.android.resources.ScreenSize; 49 import com.android.sdklib.IAndroidTarget; 50 import com.android.utils.Pair; 51 import com.android.xml.AndroidManifest; 52 53 import org.eclipse.core.resources.IFile; 54 import org.eclipse.core.resources.IProject; 55 import org.eclipse.core.resources.IResource; 56 import org.eclipse.core.resources.IWorkspace; 57 import org.eclipse.core.resources.ResourcesPlugin; 58 import org.eclipse.core.runtime.CoreException; 59 import org.eclipse.core.runtime.IPath; 60 import org.eclipse.core.runtime.NullProgressMonitor; 61 import org.eclipse.core.runtime.OperationCanceledException; 62 import org.eclipse.core.runtime.QualifiedName; 63 import org.eclipse.jdt.core.IField; 64 import org.eclipse.jdt.core.IJavaElement; 65 import org.eclipse.jdt.core.IJavaProject; 66 import org.eclipse.jdt.core.IMethod; 67 import org.eclipse.jdt.core.IPackageFragment; 68 import org.eclipse.jdt.core.IPackageFragmentRoot; 69 import org.eclipse.jdt.core.IType; 70 import org.eclipse.jdt.core.ITypeHierarchy; 71 import org.eclipse.jdt.core.search.IJavaSearchScope; 72 import org.eclipse.jdt.core.search.SearchEngine; 73 import org.eclipse.jdt.core.search.SearchMatch; 74 import org.eclipse.jdt.core.search.SearchParticipant; 75 import org.eclipse.jdt.core.search.SearchPattern; 76 import org.eclipse.jdt.core.search.SearchRequestor; 77 import org.eclipse.jdt.internal.core.BinaryType; 78 import org.eclipse.jface.text.IDocument; 79 import org.eclipse.ui.editors.text.TextFileDocumentProvider; 80 import org.eclipse.ui.texteditor.IDocumentProvider; 81 import org.w3c.dom.Document; 82 import org.w3c.dom.Element; 83 import org.w3c.dom.NodeList; 84 import org.xml.sax.InputSource; 85 import org.xml.sax.SAXException; 86 87 import java.util.ArrayList; 88 import java.util.Collections; 89 import java.util.HashMap; 90 import java.util.LinkedList; 91 import java.util.List; 92 import java.util.Map; 93 import java.util.regex.Matcher; 94 import java.util.regex.Pattern; 95 96 import javax.xml.parsers.DocumentBuilder; 97 import javax.xml.parsers.DocumentBuilderFactory; 98 import javax.xml.xpath.XPathExpressionException; 99 100 /** 101 * Retrieves and caches manifest information such as the themes to be used for 102 * a given activity. 103 * 104 * @see AndroidManifest 105 */ 106 public class ManifestInfo { 107 108 public static class ActivityAttributes { 109 @Nullable 110 private final String mIcon; 111 @Nullable 112 private final String mLabel; 113 @NonNull 114 private final String mName; 115 @Nullable 116 private final String mParentActivity; 117 @Nullable 118 private final String mTheme; 119 @Nullable 120 private final String mUiOptions; 121 ActivityAttributes(Element activity, String packageName)122 public ActivityAttributes(Element activity, String packageName) { 123 124 // Get activity name. 125 String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); 126 if (name == null || name.length() == 0) { 127 throw new RuntimeException("Activity name cannot be empty"); 128 } 129 int index = name.indexOf('.'); 130 if (index <= 0 && packageName != null && !packageName.isEmpty()) { 131 name = packageName + (index == -1 ? "." : "") + name; 132 } 133 mName = name; 134 135 // Get activity icon. 136 String value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); 137 if (value != null && value.length() > 0) { 138 mIcon = value; 139 } else { 140 mIcon = null; 141 } 142 143 // Get activity label. 144 value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); 145 if (value != null && value.length() > 0) { 146 mLabel = value; 147 } else { 148 mLabel = null; 149 } 150 151 // Get activity parent. Also search the meta-data for parent info. 152 value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_PARENT_ACTIVITY_NAME); 153 if (value == null || value.length() == 0) { 154 // TODO: Not sure if meta data can be used for API Level > 16 155 NodeList metaData = activity.getElementsByTagName(NODE_METADATA); 156 for (int j = 0, m = metaData.getLength(); j < m; j++) { 157 Element data = (Element) metaData.item(j); 158 String metadataName = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); 159 if (VALUE_PARENT_ACTIVITY.equals(metadataName)) { 160 value = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_VALUE); 161 if (value != null) { 162 index = value.indexOf('.'); 163 if (index <= 0 && packageName != null && !packageName.isEmpty()) { 164 value = packageName + (index == -1 ? "." : "") + value; 165 break; 166 } 167 } 168 } 169 } 170 } 171 if (value != null && value.length() > 0) { 172 mParentActivity = value; 173 } else { 174 mParentActivity = null; 175 } 176 177 // Get activity theme. 178 value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); 179 if (value != null && value.length() > 0) { 180 mTheme = value; 181 } else { 182 mTheme = null; 183 } 184 185 // Get UI options. 186 value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_UI_OPTIONS); 187 if (value != null && value.length() > 0) { 188 mUiOptions = value; 189 } else { 190 mUiOptions = null; 191 } 192 } 193 194 @Nullable getIcon()195 public String getIcon() { 196 return mIcon; 197 } 198 199 @Nullable getLabel()200 public String getLabel() { 201 return mLabel; 202 } 203 getName()204 public String getName() { 205 return mName; 206 } 207 208 @Nullable getParentActivity()209 public String getParentActivity() { 210 return mParentActivity; 211 } 212 213 @Nullable getTheme()214 public String getTheme() { 215 return mTheme; 216 } 217 218 @Nullable getUiOptions()219 public String getUiOptions() { 220 return mUiOptions; 221 } 222 } 223 224 /** 225 * The maximum number of milliseconds to search for an activity in the codebase when 226 * attempting to associate layouts with activities in 227 * {@link #guessActivity(IFile, String)} 228 */ 229 private static final int SEARCH_TIMEOUT_MS = 3000; 230 231 private final IProject mProject; 232 private String mPackage; 233 private String mManifestTheme; 234 private Map<String, ActivityAttributes> mActivityAttributes; 235 private IAbstractFile mManifestFile; 236 private long mLastModified; 237 private long mLastChecked; 238 private String mMinSdkName; 239 private int mMinSdk; 240 private int mTargetSdk; 241 private String mApplicationIcon; 242 private String mApplicationLabel; 243 private boolean mApplicationSupportsRtl; 244 245 /** 246 * Qualified name for the per-project non-persistent property storing the 247 * {@link ManifestInfo} for this project 248 */ 249 final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, 250 "manifest"); //$NON-NLS-1$ 251 252 /** 253 * Constructs an {@link ManifestInfo} for the given project. Don't use this method; 254 * use the {@link #get} factory method instead. 255 * 256 * @param project project to create an {@link ManifestInfo} for 257 */ ManifestInfo(IProject project)258 private ManifestInfo(IProject project) { 259 mProject = project; 260 } 261 262 /** 263 * Clears the cached manifest information. The next get call on one of the 264 * properties will cause the information to be refreshed. 265 */ clear()266 public void clear() { 267 mLastChecked = 0; 268 } 269 270 /** 271 * Returns the {@link ManifestInfo} for the given project 272 * 273 * @param project the project the finder is associated with 274 * @return a {@ManifestInfo} for the given project, never null 275 */ 276 @NonNull get(IProject project)277 public static ManifestInfo get(IProject project) { 278 ManifestInfo finder = null; 279 try { 280 finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER); 281 } catch (CoreException e) { 282 // Not a problem; we will just create a new one 283 } 284 285 if (finder == null) { 286 finder = new ManifestInfo(project); 287 try { 288 project.setSessionProperty(MANIFEST_FINDER, finder); 289 } catch (CoreException e) { 290 AdtPlugin.log(e, "Can't store ManifestInfo"); 291 } 292 } 293 294 return finder; 295 } 296 297 /** 298 * Ensure that the package, theme and activity maps are initialized and up to date 299 * with respect to the manifest file 300 */ sync()301 private void sync() { 302 // Since each of the accessors call sync(), allow a bunch of immediate 303 // accessors to all bypass the file stat() below 304 long now = System.currentTimeMillis(); 305 if (now - mLastChecked < 50 && mManifestFile != null) { 306 return; 307 } 308 mLastChecked = now; 309 310 if (mManifestFile == null) { 311 IFolderWrapper projectFolder = new IFolderWrapper(mProject); 312 mManifestFile = AndroidManifest.getManifest(projectFolder); 313 if (mManifestFile == null) { 314 return; 315 } 316 } 317 318 // Check to see if our data is up to date 319 long fileModified = mManifestFile.getModificationStamp(); 320 if (fileModified == mLastModified) { 321 // Already have up to date data 322 return; 323 } 324 mLastModified = fileModified; 325 326 mActivityAttributes = new HashMap<String, ActivityAttributes>(); 327 mManifestTheme = null; 328 mTargetSdk = 1; // Default when not specified 329 mMinSdk = 1; // Default when not specified 330 mMinSdkName = "1"; // Default when not specified 331 mPackage = ""; //$NON-NLS-1$ 332 mApplicationIcon = null; 333 mApplicationLabel = null; 334 mApplicationSupportsRtl = false; 335 336 Document document = null; 337 try { 338 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 339 InputSource is = new InputSource(mManifestFile.getContents()); 340 341 factory.setNamespaceAware(true); 342 factory.setValidating(false); 343 DocumentBuilder builder = factory.newDocumentBuilder(); 344 document = builder.parse(is); 345 346 Element root = document.getDocumentElement(); 347 mPackage = root.getAttribute(ATTRIBUTE_PACKAGE); 348 NodeList activities = document.getElementsByTagName(NODE_ACTIVITY); 349 for (int i = 0, n = activities.getLength(); i < n; i++) { 350 Element activity = (Element) activities.item(i); 351 ActivityAttributes info = new ActivityAttributes(activity, mPackage); 352 mActivityAttributes.put(info.getName(), info); 353 } 354 355 NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION); 356 if (applications.getLength() > 0) { 357 assert applications.getLength() == 1; 358 Element application = (Element) applications.item(0); 359 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) { 360 mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); 361 } 362 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) { 363 mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); 364 } 365 if (SdkConstants.VALUE_TRUE.equals(application.getAttributeNS(NS_RESOURCES, 366 ATTRIBUTE_SUPPORTS_RTL))) { 367 mApplicationSupportsRtl = true; 368 } 369 370 String defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); 371 if (defaultTheme != null && !defaultTheme.isEmpty()) { 372 // From manifest theme documentation: 373 // "If that attribute is also not set, the default system theme is used." 374 mManifestTheme = defaultTheme; 375 } 376 } 377 378 // Look up target SDK 379 NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK); 380 if (usesSdks.getLength() > 0) { 381 Element usesSdk = (Element) usesSdks.item(0); 382 mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1); 383 mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk); 384 } 385 386 } catch (SAXException e) { 387 AdtPlugin.log(e, "Malformed manifest"); 388 } catch (Exception e) { 389 AdtPlugin.log(e, "Could not read Manifest data"); 390 } 391 } 392 getApiVersion(Element usesSdk, String attribute, int defaultApiLevel)393 private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { 394 String valueString = null; 395 if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) { 396 valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute); 397 if (attribute.equals(ATTRIBUTE_MIN_SDK_VERSION)) { 398 mMinSdkName = valueString; 399 } 400 } 401 402 if (valueString != null) { 403 int apiLevel = -1; 404 try { 405 apiLevel = Integer.valueOf(valueString); 406 } catch (NumberFormatException e) { 407 // Handle codename 408 if (Sdk.getCurrent() != null) { 409 IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( 410 "android-" + valueString); //$NON-NLS-1$ 411 if (target != null) { 412 // codename future API level is current api + 1 413 apiLevel = target.getVersion().getApiLevel() + 1; 414 } 415 } 416 } 417 418 return apiLevel; 419 } 420 421 return defaultApiLevel; 422 } 423 424 /** 425 * Returns the default package registered in the Android manifest 426 * 427 * @return the default package registered in the manifest 428 */ 429 @NonNull getPackage()430 public String getPackage() { 431 sync(); 432 return mPackage; 433 } 434 435 /** 436 * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}. 437 * 438 * @return a map from activity fqcn to ActivityAttributes 439 */ 440 @NonNull getActivityAttributesMap()441 public Map<String, ActivityAttributes> getActivityAttributesMap() { 442 sync(); 443 return mActivityAttributes; 444 } 445 446 /** 447 * Returns the attributes of an activity given its full class name. 448 */ 449 @Nullable getActivityAttributes(String activity)450 public ActivityAttributes getActivityAttributes(String activity) { 451 return getActivityAttributesMap().get(activity); 452 } 453 454 /** 455 * Returns the manifest theme registered on the application, if any 456 * 457 * @return a manifest theme, or null if none was registered 458 */ 459 @Nullable getManifestTheme()460 public String getManifestTheme() { 461 sync(); 462 return mManifestTheme; 463 } 464 465 /** 466 * Returns the default theme for this project, by looking at the manifest default 467 * theme registration, target SDK, rendering target, etc. 468 * 469 * @param renderingTarget the rendering target use to render the theme, or null 470 * @param screenSize the screen size to obtain a default theme for, or null if unknown 471 * @return the theme to use for this project, never null 472 */ 473 @NonNull getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize)474 public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) { 475 sync(); 476 477 if (mManifestTheme != null) { 478 return mManifestTheme; 479 } 480 481 int renderingTargetSdk = mTargetSdk; 482 if (renderingTarget != null) { 483 renderingTargetSdk = renderingTarget.getVersion().getApiLevel(); 484 } 485 486 int apiLevel = Math.min(mTargetSdk, renderingTargetSdk); 487 // For now this theme works only on XLARGE screens. When it works for all sizes, 488 // add that new apiLevel to this check. 489 if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) { 490 return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$ 491 } else { 492 return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$ 493 } 494 } 495 496 /** 497 * Returns the application icon, or null 498 * 499 * @return the application icon, or null 500 */ 501 @Nullable getApplicationIcon()502 public String getApplicationIcon() { 503 sync(); 504 return mApplicationIcon; 505 } 506 507 /** 508 * Returns the application label, or null 509 * 510 * @return the application label, or null 511 */ 512 @Nullable getApplicationLabel()513 public String getApplicationLabel() { 514 sync(); 515 return mApplicationLabel; 516 } 517 518 /** 519 * Returns true if the application has RTL support. 520 * 521 * @return true if the application has RTL support. 522 */ isRtlSupported()523 public boolean isRtlSupported() { 524 sync(); 525 return mApplicationSupportsRtl; 526 } 527 528 /** 529 * Returns the target SDK version 530 * 531 * @return the target SDK version 532 */ getTargetSdkVersion()533 public int getTargetSdkVersion() { 534 sync(); 535 return mTargetSdk; 536 } 537 538 /** 539 * Returns the minimum SDK version 540 * 541 * @return the minimum SDK version 542 */ getMinSdkVersion()543 public int getMinSdkVersion() { 544 sync(); 545 return mMinSdk; 546 } 547 548 /** 549 * Returns the minimum SDK version name (which may not be a numeric string, e.g. 550 * it could be a codename). It will never be null or empty; if no min sdk version 551 * was specified in the manifest, the return value will be "1". Use 552 * {@link #getMinSdkCodeName()} instead if you want to look up whether there is a code name. 553 * 554 * @return the minimum SDK version 555 */ 556 @NonNull getMinSdkName()557 public String getMinSdkName() { 558 sync(); 559 if (mMinSdkName == null || mMinSdkName.isEmpty()) { 560 mMinSdkName = "1"; //$NON-NLS-1$ 561 } 562 563 return mMinSdkName; 564 } 565 566 /** 567 * Returns the code name used for the minimum SDK version, if any. 568 * 569 * @return the minSdkVersion codename or null 570 */ 571 @Nullable getMinSdkCodeName()572 public String getMinSdkCodeName() { 573 String minSdkName = getMinSdkName(); 574 if (!Character.isDigit(minSdkName.charAt(0))) { 575 return minSdkName; 576 } 577 578 return null; 579 } 580 581 /** 582 * Returns the {@link IPackageFragment} for the package registered in the manifest 583 * 584 * @return the {@link IPackageFragment} for the package registered in the manifest 585 */ 586 @Nullable getPackageFragment()587 public IPackageFragment getPackageFragment() { 588 sync(); 589 try { 590 IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); 591 if (javaProject != null) { 592 IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); 593 if (root != null) { 594 return root.getPackageFragment(mPackage); 595 } 596 } 597 } catch (CoreException e) { 598 AdtPlugin.log(e, null); 599 } 600 601 return null; 602 } 603 604 /** 605 * Returns the activity associated with the given layout file. Makes an educated guess 606 * by peeking at the usages of the R.layout.name field corresponding to the layout and 607 * if it finds a usage. 608 * 609 * @param project the project containing the layout 610 * @param layoutName the layout whose activity we want to look up 611 * @param pkg the package containing activities 612 * @return the activity name 613 */ 614 @Nullable guessActivity(IProject project, String layoutName, String pkg)615 public static String guessActivity(IProject project, String layoutName, String pkg) { 616 List<String> activities = guessActivities(project, layoutName, pkg); 617 if (activities.size() > 0) { 618 return activities.get(0); 619 } else { 620 return null; 621 } 622 } 623 624 /** 625 * Returns the activities associated with the given layout file. Makes an educated guess 626 * by peeking at the usages of the R.layout.name field corresponding to the layout and 627 * if it finds a usage. 628 * 629 * @param project the project containing the layout 630 * @param layoutName the layout whose activity we want to look up 631 * @param pkg the package containing activities 632 * @return the activity name 633 */ 634 @NonNull guessActivities(IProject project, String layoutName, String pkg)635 public static List<String> guessActivities(IProject project, String layoutName, String pkg) { 636 final LinkedList<String> activities = new LinkedList<String>(); 637 SearchRequestor requestor = new SearchRequestor() { 638 @Override 639 public void acceptSearchMatch(SearchMatch match) throws CoreException { 640 Object element = match.getElement(); 641 if (element instanceof IMethod) { 642 IMethod method = (IMethod) element; 643 IType declaringType = method.getDeclaringType(); 644 String fqcn = declaringType.getFullyQualifiedName(); 645 646 if ((declaringType.getSuperclassName() != null && 647 declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$ 648 || method.getElementName().equals("onCreate")) { //$NON-NLS-1$ 649 activities.addFirst(fqcn); 650 } else { 651 activities.addLast(fqcn); 652 } 653 } 654 } 655 }; 656 try { 657 IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); 658 if (javaProject == null) { 659 return Collections.emptyList(); 660 } 661 // TODO - look around a bit more and see if we can figure out whether the 662 // call if from within a setContentView call! 663 664 // Search for which java classes call setContentView(R.layout.layoutname); 665 String typeFqcn = "R.layout"; //$NON-NLS-1$ 666 if (pkg != null) { 667 typeFqcn = pkg + '.' + typeFqcn; 668 } 669 670 IType type = javaProject.findType(typeFqcn); 671 if (type != null) { 672 IField field = type.getField(layoutName); 673 if (field.exists()) { 674 SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES); 675 try { 676 search(requestor, javaProject, pattern); 677 } catch (OperationCanceledException canceled) { 678 // pass 679 } 680 } 681 } 682 } catch (CoreException e) { 683 AdtPlugin.log(e, null); 684 } 685 686 return activities; 687 } 688 689 /** 690 * Returns all activities found in the given project (including those in libraries, 691 * except for android.jar itself) 692 * 693 * @param project the project 694 * @return a list of activity classes as fully qualified class names 695 */ 696 @SuppressWarnings("restriction") // BinaryType 697 @NonNull getProjectActivities(IProject project)698 public static List<String> getProjectActivities(IProject project) { 699 final List<String> activities = new ArrayList<String>(); 700 try { 701 final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); 702 if (javaProject != null) { 703 IType[] activityTypes = new IType[0]; 704 IType activityType = javaProject.findType(CLASS_ACTIVITY); 705 if (activityType != null) { 706 ITypeHierarchy hierarchy = 707 activityType.newTypeHierarchy(javaProject, new NullProgressMonitor()); 708 activityTypes = hierarchy.getAllSubtypes(activityType); 709 for (IType type : activityTypes) { 710 if (type instanceof BinaryType && (type.getClassFile() == null 711 || type.getClassFile().getResource() == null)) { 712 continue; 713 } 714 activities.add(type.getFullyQualifiedName()); 715 } 716 } 717 } 718 } catch (CoreException e) { 719 AdtPlugin.log(e, null); 720 } 721 722 return activities; 723 } 724 725 726 /** 727 * Returns the activity associated with the given layout file. 728 * <p> 729 * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas 730 * guessActivity simply looks for references to "R.layout.foo", this method searches 731 * for all usages of Activity#setContentView(int), and for each match it looks up the 732 * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses 733 * a regexp to pull out "foo" from this, and stores the association that layout "foo" 734 * is associated with the activity class that contained the setContentView call. 735 * <p> 736 * This has two potential advantages: 737 * <ol> 738 * <li>It can be faster. We do the reference search -once-, and we've built a map of 739 * all the layout-to-activity mappings which we can then immediately look up other 740 * layouts for, which is particularly useful at startup when we have to compute the 741 * layout activity associations to populate the theme choosers. 742 * <li>It can be more accurate. Just because an activity references an "R.layout.foo" 743 * field doesn't mean it's setting it as a content view. 744 * </ol> 745 * However, this second advantage is also its chief problem. There are some common 746 * code constructs which means that the associated layout is not explicitly referenced 747 * in a direct setContentView call; on a couple of sample projects I tested I found 748 * patterns like for example "setContentView(v)" where "v" had been computed earlier. 749 * Therefore, for now we're going to stick with the more general approach of just 750 * looking up each field when needed. We're keeping the code around, though statically 751 * compiled out with the "if (false)" construct below in case we revisit this. 752 * 753 * @param layoutFile the layout whose activity we want to look up 754 * @return the activity name 755 */ 756 @SuppressWarnings("all") 757 @Nullable guessActivityBySetContentView(String layoutName)758 public String guessActivityBySetContentView(String layoutName) { 759 if (false) { 760 // These should be fields 761 final Pattern LAYOUT_FIELD_PATTERN = 762 Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$ 763 Map<String, String> mUsages = null; 764 765 sync(); 766 if (mUsages == null) { 767 final Map<String, String> usages = new HashMap<String, String>(); 768 mUsages = usages; 769 SearchRequestor requestor = new SearchRequestor() { 770 @Override 771 public void acceptSearchMatch(SearchMatch match) throws CoreException { 772 Object element = match.getElement(); 773 if (element instanceof IMethod) { 774 IMethod method = (IMethod) element; 775 IType declaringType = method.getDeclaringType(); 776 String fqcn = declaringType.getFullyQualifiedName(); 777 IDocumentProvider provider = new TextFileDocumentProvider(); 778 IResource resource = match.getResource(); 779 try { 780 provider.connect(resource); 781 IDocument document = provider.getDocument(resource); 782 if (document != null) { 783 String matchText = document.get(match.getOffset(), 784 match.getLength()); 785 Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText); 786 if (matcher.find()) { 787 usages.put(matcher.group(1), fqcn); 788 } 789 } 790 } catch (Exception e) { 791 AdtPlugin.log(e, "Can't find range information for %1$s", 792 resource.getName()); 793 } finally { 794 provider.disconnect(resource); 795 } 796 } 797 } 798 }; 799 try { 800 IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); 801 if (javaProject == null) { 802 return null; 803 } 804 805 // Search for which java classes call setContentView(R.layout.layoutname); 806 String typeFqcn = "R.layout"; //$NON-NLS-1$ 807 if (mPackage != null) { 808 typeFqcn = mPackage + '.' + typeFqcn; 809 } 810 811 IType activityType = javaProject.findType(CLASS_ACTIVITY); 812 if (activityType != null) { 813 IMethod method = activityType.getMethod( 814 "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$ 815 if (method.exists()) { 816 SearchPattern pattern = SearchPattern.createPattern(method, 817 REFERENCES); 818 search(requestor, javaProject, pattern); 819 } 820 } 821 } catch (CoreException e) { 822 AdtPlugin.log(e, null); 823 } 824 } 825 826 return mUsages.get(layoutName); 827 } 828 829 return null; 830 } 831 832 /** 833 * Performs a search using the given pattern, scope and handler. The search will abort 834 * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds. 835 */ search(SearchRequestor requestor, IJavaProject javaProject, SearchPattern pattern)836 private static void search(SearchRequestor requestor, IJavaProject javaProject, 837 SearchPattern pattern) throws CoreException { 838 // Find the package fragment specified in the manifest; the activities should 839 // live there. 840 IJavaSearchScope scope = createPackageScope(javaProject); 841 842 SearchParticipant[] participants = new SearchParticipant[] { 843 SearchEngine.getDefaultSearchParticipant() 844 }; 845 SearchEngine engine = new SearchEngine(); 846 847 final long searchStart = System.currentTimeMillis(); 848 NullProgressMonitor monitor = new NullProgressMonitor() { 849 private boolean mCancelled; 850 @Override 851 public void internalWorked(double work) { 852 long searchEnd = System.currentTimeMillis(); 853 if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) { 854 mCancelled = true; 855 } 856 } 857 858 @Override 859 public boolean isCanceled() { 860 return mCancelled; 861 } 862 }; 863 engine.search(pattern, participants, scope, requestor, monitor); 864 } 865 866 /** Creates a package search scope for the first package root in the given java project */ createPackageScope(IJavaProject javaProject)867 private static IJavaSearchScope createPackageScope(IJavaProject javaProject) { 868 IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject); 869 870 IJavaSearchScope scope; 871 if (packageRoot != null) { 872 IJavaElement[] scopeElements = new IJavaElement[] { packageRoot }; 873 scope = SearchEngine.createJavaSearchScope(scopeElements); 874 } else { 875 scope = SearchEngine.createWorkspaceScope(); 876 } 877 return scope; 878 } 879 880 /** 881 * Returns the first package root for the given java project 882 * 883 * @param javaProject the project to search in 884 * @return the first package root, or null 885 */ 886 @Nullable getSourcePackageRoot(IJavaProject javaProject)887 public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) { 888 IPackageFragmentRoot packageRoot = null; 889 List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject); 890 891 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 892 for (IPath path : sources) { 893 IResource firstSource = workspace.getRoot().findMember(path); 894 if (firstSource != null) { 895 packageRoot = javaProject.getPackageFragmentRoot(firstSource); 896 if (packageRoot != null) { 897 break; 898 } 899 } 900 } 901 return packageRoot; 902 } 903 904 /** 905 * Computes the minimum SDK and target SDK versions for the project 906 * 907 * @param project the project to look up the versions for 908 * @return a pair of (minimum SDK, target SDK) versions, never null 909 */ 910 @NonNull computeSdkVersions(IProject project)911 public static Pair<Integer, Integer> computeSdkVersions(IProject project) { 912 int mMinSdkVersion = 1; 913 int mTargetSdkVersion = 1; 914 915 IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project)); 916 if (manifestFile != null) { 917 try { 918 Object value = AndroidManifest.getMinSdkVersion(manifestFile); 919 mMinSdkVersion = 1; // Default case if missing 920 if (value instanceof Integer) { 921 mMinSdkVersion = ((Integer) value).intValue(); 922 } else if (value instanceof String) { 923 // handle codename, only if we can resolve it. 924 if (Sdk.getCurrent() != null) { 925 IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( 926 "android-" + value); //$NON-NLS-1$ 927 if (target != null) { 928 // codename future API level is current api + 1 929 mMinSdkVersion = target.getVersion().getApiLevel() + 1; 930 } 931 } 932 } 933 934 value = AndroidManifest.getTargetSdkVersion(manifestFile); 935 if (value == null) { 936 mTargetSdkVersion = mMinSdkVersion; 937 } else if (value instanceof String) { 938 // handle codename, only if we can resolve it. 939 if (Sdk.getCurrent() != null) { 940 IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( 941 "android-" + value); //$NON-NLS-1$ 942 if (target != null) { 943 // codename future API level is current api + 1 944 mTargetSdkVersion = target.getVersion().getApiLevel() + 1; 945 } 946 } 947 } 948 } catch (XPathExpressionException e) { 949 // do nothing we'll use 1 below. 950 } catch (StreamException e) { 951 // do nothing we'll use 1 below. 952 } 953 } 954 955 return Pair.of(mMinSdkVersion, mTargetSdkVersion); 956 } 957 } 958