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.ide.common.resources.ResourceResolver.PREFIX_ANDROID_STYLE; 20 import static com.android.sdklib.SdkConstants.NS_RESOURCES; 21 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_ICON; 22 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_LABEL; 23 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; 24 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME; 25 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE; 26 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; 27 import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_THEME; 28 import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY; 29 import static com.android.sdklib.xml.AndroidManifest.NODE_USES_SDK; 30 import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES; 31 32 import com.android.ide.eclipse.adt.AdtPlugin; 33 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 34 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 35 import com.android.ide.eclipse.adt.io.IFolderWrapper; 36 import com.android.io.IAbstractFile; 37 import com.android.io.StreamException; 38 import com.android.resources.ScreenSize; 39 import com.android.sdklib.IAndroidTarget; 40 import com.android.sdklib.SdkConstants; 41 import com.android.sdklib.xml.AndroidManifest; 42 import com.android.util.Pair; 43 44 import org.eclipse.core.resources.IFile; 45 import org.eclipse.core.resources.IProject; 46 import org.eclipse.core.resources.IResource; 47 import org.eclipse.core.resources.IWorkspace; 48 import org.eclipse.core.resources.ResourcesPlugin; 49 import org.eclipse.core.runtime.CoreException; 50 import org.eclipse.core.runtime.IPath; 51 import org.eclipse.core.runtime.NullProgressMonitor; 52 import org.eclipse.core.runtime.QualifiedName; 53 import org.eclipse.jdt.core.IField; 54 import org.eclipse.jdt.core.IJavaElement; 55 import org.eclipse.jdt.core.IJavaProject; 56 import org.eclipse.jdt.core.IMethod; 57 import org.eclipse.jdt.core.IPackageFragment; 58 import org.eclipse.jdt.core.IPackageFragmentRoot; 59 import org.eclipse.jdt.core.IType; 60 import org.eclipse.jdt.core.search.IJavaSearchScope; 61 import org.eclipse.jdt.core.search.SearchEngine; 62 import org.eclipse.jdt.core.search.SearchMatch; 63 import org.eclipse.jdt.core.search.SearchParticipant; 64 import org.eclipse.jdt.core.search.SearchPattern; 65 import org.eclipse.jdt.core.search.SearchRequestor; 66 import org.eclipse.jface.text.IDocument; 67 import org.eclipse.ui.editors.text.TextFileDocumentProvider; 68 import org.eclipse.ui.texteditor.IDocumentProvider; 69 import org.w3c.dom.Document; 70 import org.w3c.dom.Element; 71 import org.w3c.dom.NodeList; 72 import org.xml.sax.InputSource; 73 import org.xml.sax.SAXException; 74 75 import java.util.HashMap; 76 import java.util.List; 77 import java.util.Map; 78 import java.util.concurrent.atomic.AtomicReference; 79 import java.util.regex.Matcher; 80 import java.util.regex.Pattern; 81 82 import javax.xml.parsers.DocumentBuilder; 83 import javax.xml.parsers.DocumentBuilderFactory; 84 import javax.xml.xpath.XPathExpressionException; 85 86 /** 87 * Retrieves and caches manifest information such as the themes to be used for 88 * a given activity. 89 * 90 * @see AndroidManifest 91 */ 92 public class ManifestInfo { 93 /** 94 * The maximum number of milliseconds to search for an activity in the codebase when 95 * attempting to associate layouts with activities in 96 * {@link #guessActivity(IFile, String)} 97 */ 98 private static final int SEARCH_TIMEOUT_MS = 3000; 99 100 private final IProject mProject; 101 private String mPackage; 102 private String mManifestTheme; 103 private Map<String, String> mActivityThemes; 104 private IAbstractFile mManifestFile; 105 private long mLastModified; 106 private int mTargetSdk; 107 private String mApplicationIcon; 108 private String mApplicationLabel; 109 110 /** 111 * Qualified name for the per-project non-persistent property storing the 112 * {@link ManifestInfo} for this project 113 */ 114 final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, 115 "manifest"); //$NON-NLS-1$ 116 117 /** 118 * Constructs an {@link ManifestInfo} for the given project. Don't use this method; 119 * use the {@link #get} factory method instead. 120 * 121 * @param project project to create an {@link ManifestInfo} for 122 */ 123 private ManifestInfo(IProject project) { 124 mProject = project; 125 } 126 127 /** 128 * Returns the {@link ManifestInfo} for the given project 129 * 130 * @param project the project the finder is associated with 131 * @return a {@ManifestInfo} for the given project, never null 132 */ 133 public static ManifestInfo get(IProject project) { 134 ManifestInfo finder = null; 135 try { 136 finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER); 137 } catch (CoreException e) { 138 // Not a problem; we will just create a new one 139 } 140 141 if (finder == null) { 142 finder = new ManifestInfo(project); 143 try { 144 project.setSessionProperty(MANIFEST_FINDER, finder); 145 } catch (CoreException e) { 146 AdtPlugin.log(e, "Can't store ManifestInfo"); 147 } 148 } 149 150 return finder; 151 } 152 153 /** 154 * Ensure that the package, theme and activity maps are initialized and up to date 155 * with respect to the manifest file 156 */ 157 private void sync() { 158 if (mManifestFile == null) { 159 IFolderWrapper projectFolder = new IFolderWrapper(mProject); 160 mManifestFile = AndroidManifest.getManifest(projectFolder); 161 if (mManifestFile == null) { 162 return; 163 } 164 } 165 166 // Check to see if our data is up to date 167 long fileModified = mManifestFile.getModificationStamp(); 168 if (fileModified == mLastModified) { 169 // Already have up to date data 170 return; 171 } 172 mLastModified = fileModified; 173 174 mActivityThemes = new HashMap<String, String>(); 175 mManifestTheme = null; 176 mTargetSdk = 1; // Default when not specified 177 mPackage = ""; //$NON-NLS-1$ 178 mApplicationIcon = null; 179 mApplicationLabel = null; 180 181 Document document = null; 182 try { 183 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 184 InputSource is = new InputSource(mManifestFile.getContents()); 185 186 factory.setNamespaceAware(true); 187 factory.setValidating(false); 188 DocumentBuilder builder = factory.newDocumentBuilder(); 189 document = builder.parse(is); 190 191 Element root = document.getDocumentElement(); 192 mPackage = root.getAttribute(ATTRIBUTE_PACKAGE); 193 NodeList activities = document.getElementsByTagName(NODE_ACTIVITY); 194 for (int i = 0, n = activities.getLength(); i < n; i++) { 195 Element activity = (Element) activities.item(i); 196 String theme = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); 197 if (theme != null && theme.length() > 0) { 198 String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); 199 if (name.startsWith(".") //$NON-NLS-1$ 200 && mPackage != null && mPackage.length() > 0) { 201 name = mPackage + name; 202 } 203 mActivityThemes.put(name, theme); 204 } 205 } 206 207 NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION); 208 if (applications.getLength() > 0) { 209 assert applications.getLength() == 1; 210 Element application = (Element) applications.item(0); 211 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) { 212 mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); 213 } 214 if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) { 215 mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); 216 } 217 } 218 219 // Look up target SDK 220 String defaultTheme = root.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); 221 if (defaultTheme == null || defaultTheme.length() == 0) { 222 // From manifest theme documentation: 223 // "If that attribute is also not set, the default system theme is used." 224 225 NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK); 226 if (usesSdks.getLength() > 0) { 227 Element usesSdk = (Element) usesSdks.item(0); 228 String targetSdk = null; 229 if (usesSdk.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_TARGET_SDK_VERSION)) { 230 targetSdk = usesSdk.getAttributeNS(NS_RESOURCES, 231 ATTRIBUTE_TARGET_SDK_VERSION); 232 } else if (usesSdk.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_MIN_SDK_VERSION)) { 233 targetSdk = usesSdk.getAttributeNS(NS_RESOURCES, 234 ATTRIBUTE_MIN_SDK_VERSION); 235 } 236 if (targetSdk != null) { 237 int apiLevel = -1; 238 try { 239 apiLevel = Integer.valueOf(targetSdk); 240 } catch (NumberFormatException e) { 241 // Handle codename 242 if (Sdk.getCurrent() != null) { 243 IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( 244 "android-" + targetSdk); //$NON-NLS-1$ 245 if (target != null) { 246 // codename future API level is current api + 1 247 apiLevel = target.getVersion().getApiLevel() + 1; 248 } 249 } 250 } 251 252 mTargetSdk = apiLevel; 253 } 254 } 255 } else { 256 mManifestTheme = defaultTheme; 257 } 258 } catch (SAXException e) { 259 AdtPlugin.log(e, "Malformed manifest"); 260 } catch (Exception e) { 261 AdtPlugin.log(e, "Could not read Manifest data"); 262 } 263 } 264 265 /** 266 * Returns the default package registered in the Android manifest 267 * 268 * @return the default package registered in the manifest 269 */ 270 public String getPackage() { 271 sync(); 272 return mPackage; 273 } 274 275 /** 276 * Returns a map from activity full class names to the corresponding theme style to be 277 * used 278 * 279 * @return a map from activity fqcn to theme style 280 */ 281 public Map<String, String> getActivityThemes() { 282 sync(); 283 return mActivityThemes; 284 } 285 286 /** 287 * Returns the default theme for this project, by looking at the manifest default 288 * theme registration, target SDK, rendering target, etc. 289 * 290 * @param renderingTarget the rendering target use to render the theme, or null 291 * @param screenSize the screen size to obtain a default theme for, or null if unknown 292 * @return the theme to use for this project, never null 293 */ 294 public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) { 295 sync(); 296 297 if (mManifestTheme != null) { 298 return mManifestTheme; 299 } 300 301 int renderingTargetSdk = mTargetSdk; 302 if (renderingTarget != null) { 303 renderingTargetSdk = renderingTarget.getVersion().getApiLevel(); 304 } 305 306 int apiLevel = Math.min(mTargetSdk, renderingTargetSdk); 307 // For now this theme works only on XLARGE screens. When it works for all sizes, 308 // add that new apiLevel to this check. 309 if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE) { 310 return PREFIX_ANDROID_STYLE + "Theme.Holo"; //$NON-NLS-1$ 311 } else { 312 return PREFIX_ANDROID_STYLE + "Theme"; //$NON-NLS-1$ 313 } 314 } 315 316 /** 317 * Returns the application icon, or null 318 * 319 * @return the application icon, or null 320 */ 321 public String getApplicationIcon() { 322 sync(); 323 return mApplicationIcon; 324 } 325 326 /** 327 * Returns the application label, or null 328 * 329 * @return the application label, or null 330 */ 331 public String getApplicationLabel() { 332 sync(); 333 return mApplicationLabel; 334 } 335 336 /** 337 * Returns the {@link IPackageFragment} for the package registered in the manifest 338 * 339 * @return the {@link IPackageFragment} for the package registered in the manifest 340 */ 341 public IPackageFragment getPackageFragment() { 342 sync(); 343 try { 344 IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); 345 if (javaProject != null) { 346 IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); 347 if (root != null) { 348 return root.getPackageFragment(mPackage); 349 } 350 } 351 } catch (CoreException e) { 352 AdtPlugin.log(e, null); 353 } 354 355 return null; 356 } 357 358 /** 359 * Returns the activity associated with the given layout file. Makes an educated guess 360 * by peeking at the usages of the R.layout.name field corresponding to the layout and 361 * if it finds a usage. 362 * 363 * @param project the project containing the layout 364 * @param layoutName the layout whose activity we want to look up 365 * @param pkg the package containing activities 366 * @return the activity name 367 */ 368 public static String guessActivity(IProject project, String layoutName, String pkg) { 369 final AtomicReference<String> activity = new AtomicReference<String>(); 370 SearchRequestor requestor = new SearchRequestor() { 371 @Override 372 public void acceptSearchMatch(SearchMatch match) throws CoreException { 373 Object element = match.getElement(); 374 if (element instanceof IMethod) { 375 IMethod method = (IMethod) element; 376 IType declaringType = method.getDeclaringType(); 377 String fqcn = declaringType.getFullyQualifiedName(); 378 if (activity.get() == null 379 || (declaringType.getSuperclassName() != null && 380 declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$ 381 || method.getElementName().equals("onCreate")) { //$NON-NLS-1$ 382 activity.set(fqcn); 383 } 384 } 385 } 386 }; 387 try { 388 IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); 389 if (javaProject == null) { 390 return null; 391 } 392 // TODO - look around a bit more and see if we can figure out whether the 393 // call if from within a setContentView call! 394 395 // Search for which java classes call setContentView(R.layout.layoutname); 396 String typeFqcn = "R.layout"; //$NON-NLS-1$ 397 if (pkg != null) { 398 typeFqcn = pkg + '.' + typeFqcn; 399 } 400 401 IType type = javaProject.findType(typeFqcn); 402 if (type != null) { 403 IField field = type.getField(layoutName); 404 if (field.exists()) { 405 SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES); 406 search(requestor, javaProject, pattern); 407 } 408 } 409 } catch (CoreException e) { 410 AdtPlugin.log(e, null); 411 } 412 413 return activity.get(); 414 } 415 416 /** 417 * Returns the activity associated with the given layout file. 418 * <p> 419 * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas 420 * guessActivity simply looks for references to "R.layout.foo", this method searches 421 * for all usages of Activity#setContentView(int), and for each match it looks up the 422 * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses 423 * a regexp to pull out "foo" from this, and stores the association that layout "foo" 424 * is associated with the activity class that contained the setContentView call. 425 * <p> 426 * This has two potential advantages: 427 * <ol> 428 * <li>It can be faster. We do the reference search -once-, and we've built a map of 429 * all the layout-to-activity mappings which we can then immediately look up other 430 * layouts for, which is particularly useful at startup when we have to compute the 431 * layout activity associations to populate the theme choosers. 432 * <li>It can be more accurate. Just because an activity references an "R.layout.foo" 433 * field doesn't mean it's setting it as a content view. 434 * </ol> 435 * However, this second advantage is also its chief problem. There are some common 436 * code constructs which means that the associated layout is not explicitly referenced 437 * in a direct setContentView call; on a couple of sample projects I tested I found 438 * patterns like for example "setContentView(v)" where "v" had been computed earlier. 439 * Therefore, for now we're going to stick with the more general approach of just 440 * looking up each field when needed. We're keeping the code around, though statically 441 * compiled out with the "if (false)" construct below in case we revisit this. 442 * 443 * @param layoutFile the layout whose activity we want to look up 444 * @return the activity name 445 */ 446 @SuppressWarnings("all") 447 public String guessActivityBySetContentView(String layoutName) { 448 if (false) { 449 // These should be fields 450 final Pattern LAYOUT_FIELD_PATTERN = 451 Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$ 452 Map<String, String> mUsages = null; 453 454 sync(); 455 if (mUsages == null) { 456 final Map<String, String> usages = new HashMap<String, String>(); 457 mUsages = usages; 458 SearchRequestor requestor = new SearchRequestor() { 459 @Override 460 public void acceptSearchMatch(SearchMatch match) throws CoreException { 461 Object element = match.getElement(); 462 if (element instanceof IMethod) { 463 IMethod method = (IMethod) element; 464 IType declaringType = method.getDeclaringType(); 465 String fqcn = declaringType.getFullyQualifiedName(); 466 IDocumentProvider provider = new TextFileDocumentProvider(); 467 IResource resource = match.getResource(); 468 try { 469 provider.connect(resource); 470 IDocument document = provider.getDocument(resource); 471 if (document != null) { 472 String matchText = document.get(match.getOffset(), 473 match.getLength()); 474 Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText); 475 if (matcher.find()) { 476 usages.put(matcher.group(1), fqcn); 477 } 478 } 479 } catch (Exception e) { 480 AdtPlugin.log(e, "Can't find range information for %1$s", 481 resource.getName()); 482 } finally { 483 provider.disconnect(resource); 484 } 485 } 486 } 487 }; 488 try { 489 IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); 490 if (javaProject == null) { 491 return null; 492 } 493 494 // Search for which java classes call setContentView(R.layout.layoutname); 495 String typeFqcn = "R.layout"; //$NON-NLS-1$ 496 if (mPackage != null) { 497 typeFqcn = mPackage + '.' + typeFqcn; 498 } 499 500 IType activityType = javaProject.findType(SdkConstants.CLASS_ACTIVITY); 501 if (activityType != null) { 502 IMethod method = activityType.getMethod( 503 "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$ 504 if (method.exists()) { 505 SearchPattern pattern = SearchPattern.createPattern(method, 506 REFERENCES); 507 search(requestor, javaProject, pattern); 508 } 509 } 510 } catch (CoreException e) { 511 AdtPlugin.log(e, null); 512 } 513 } 514 515 return mUsages.get(layoutName); 516 } 517 518 return null; 519 } 520 521 /** 522 * Performs a search using the given pattern, scope and handler. The search will abort 523 * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds. 524 */ 525 private static void search(SearchRequestor requestor, IJavaProject javaProject, 526 SearchPattern pattern) throws CoreException { 527 // Find the package fragment specified in the manifest; the activities should 528 // live there. 529 IJavaSearchScope scope = createPackageScope(javaProject); 530 531 SearchParticipant[] participants = new SearchParticipant[] { 532 SearchEngine.getDefaultSearchParticipant() 533 }; 534 SearchEngine engine = new SearchEngine(); 535 536 final long searchStart = System.currentTimeMillis(); 537 NullProgressMonitor monitor = new NullProgressMonitor() { 538 private boolean mCancelled; 539 @Override 540 public void internalWorked(double work) { 541 long searchEnd = System.currentTimeMillis(); 542 if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) { 543 mCancelled = true; 544 } 545 } 546 547 @Override 548 public boolean isCanceled() { 549 return mCancelled; 550 } 551 }; 552 engine.search(pattern, participants, scope, requestor, monitor); 553 } 554 555 /** Creates a package search scope for the first package root in the given java project */ 556 private static IJavaSearchScope createPackageScope(IJavaProject javaProject) { 557 IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject); 558 559 IJavaSearchScope scope; 560 if (packageRoot != null) { 561 IJavaElement[] scopeElements = new IJavaElement[] { packageRoot }; 562 scope = SearchEngine.createJavaSearchScope(scopeElements); 563 } else { 564 scope = SearchEngine.createWorkspaceScope(); 565 } 566 return scope; 567 } 568 569 /** Returns the first package root for the given java project */ 570 public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) { 571 IPackageFragmentRoot packageRoot = null; 572 List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject); 573 574 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 575 for (IPath path : sources) { 576 IResource firstSource = workspace.getRoot().findMember(path); 577 if (firstSource != null) { 578 packageRoot = javaProject.getPackageFragmentRoot(firstSource); 579 if (packageRoot != null) { 580 break; 581 } 582 } 583 } 584 return packageRoot; 585 } 586 587 /** 588 * Computes the minimum SDK and target SDK versions for the project 589 * 590 * @return a pair of (minimum SDK, target SDK) versions, never null 591 */ 592 public static Pair<Integer, Integer> computeSdkVersions(IProject project) { 593 int mMinSdkVersion = 1; 594 int mTargetSdkVersion = 1; 595 596 IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project)); 597 if (manifestFile != null) { 598 try { 599 Object value = AndroidManifest.getMinSdkVersion(manifestFile); 600 mMinSdkVersion = 1; // Default case if missing 601 if (value instanceof Integer) { 602 mMinSdkVersion = ((Integer) value).intValue(); 603 } else if (value instanceof String) { 604 // handle codename, only if we can resolve it. 605 if (Sdk.getCurrent() != null) { 606 IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( 607 "android-" + value); //$NON-NLS-1$ 608 if (target != null) { 609 // codename future API level is current api + 1 610 mMinSdkVersion = target.getVersion().getApiLevel() + 1; 611 } 612 } 613 } 614 615 Integer i = AndroidManifest.getTargetSdkVersion(manifestFile); 616 if (i == null) { 617 mTargetSdkVersion = mMinSdkVersion; 618 } else { 619 mTargetSdkVersion = i.intValue(); 620 } 621 } catch (XPathExpressionException e) { 622 // do nothing we'll use 1 below. 623 } catch (StreamException e) { 624 // do nothing we'll use 1 below. 625 } 626 } 627 628 return Pair.of(mMinSdkVersion, mTargetSdkVersion); 629 } 630 } 631