| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.sdklib.repository.local; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.annotations.VisibleForTesting.Visibility; |
| import com.android.annotations.concurrency.GuardedBy; |
| import com.android.sdklib.AndroidTargetHash; |
| import com.android.sdklib.AndroidVersion; |
| import com.android.sdklib.AndroidVersion.AndroidVersionException; |
| import com.android.sdklib.BuildToolInfo; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.internal.repository.packages.PackageParserUtils; |
| import com.android.sdklib.io.FileOp; |
| import com.android.sdklib.io.IFileOp; |
| import com.android.sdklib.repository.FullRevision; |
| import com.android.sdklib.repository.MajorRevision; |
| import com.android.sdklib.repository.NoPreviewRevision; |
| import com.android.sdklib.repository.PkgProps; |
| import com.android.sdklib.repository.descriptors.IPkgDesc; |
| import com.android.sdklib.repository.descriptors.IdDisplay; |
| import com.android.sdklib.repository.descriptors.PkgDescExtra; |
| import com.android.sdklib.repository.descriptors.PkgType; |
| import com.android.sdklib.repository.remote.RemoteSdk; |
| import com.google.common.collect.HashMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.TreeMultimap; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.List; |
| import java.util.Properties; |
| |
| /** |
| * This class keeps information on the current locally installed SDK. |
| * It tries to lazily load information as much as possible. |
| * <p/> |
| * Packages are accessed by their type and a main query attribute, depending on the |
| * package type. There are different versions of {@link #getPkgInfo} which depend on the |
| * query attribute. |
| * |
| * <table border='1' cellpadding='3'> |
| * <tr> |
| * <th>Type</th> |
| * <th>Query parameter</th> |
| * <th>Getter</th> |
| * </tr> |
| * |
| * <tr> |
| * <td>Tools</td> |
| * <td>Unique instance</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_TOOLS)} => {@link LocalPkgInfo}</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Platform-Tools</td> |
| * <td>Unique instance</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_PLATFORM_TOOLS)} => {@link LocalPkgInfo}</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Docs</td> |
| * <td>Unique instance</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_DOCS)} => {@link LocalPkgInfo}</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Build-Tools</td> |
| * <td>{@link FullRevision}</td> |
| * <td>{@code getLatestBuildTool()} => {@link BuildToolInfo}, <br/> |
| * or {@code getBuildTool(FullRevision)} => {@link BuildToolInfo}, <br/> |
| * or {@code getPkgInfo(PkgType.PKG_BUILD_TOOLS, FullRevision)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)} => {@link LocalPkgInfo}[]</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Extras</td> |
| * <td>String vendor/path</td> |
| * <td>{@code getExtra(String)} => {@link LocalExtraPkgInfo}, <br/> |
| * or {@code getPkgInfo(PkgType.PKG_EXTRAS, String)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_EXTRAS)} => {@link LocalPkgInfo}[]</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Sources</td> |
| * <td>{@link AndroidVersion}</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_SOURCES, AndroidVersion)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_SOURCES)} => {@link LocalPkgInfo}[]</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Samples</td> |
| * <td>{@link AndroidVersion}</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_SAMPLES, AndroidVersion)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_SAMPLES)} => {@link LocalPkgInfo}[]</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Platforms</td> |
| * <td>{@link AndroidVersion}</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_PLATFORMS, AndroidVersion)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_PLATFORMS)} => {@link LocalPkgInfo}[], <br/> |
| * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Add-ons</td> |
| * <td>{@link AndroidVersion} x String vendor/path</td> |
| * <td>{@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo}, <br/> |
| * or {@code getPkgsInfos(PkgType.PKG_ADDONS)} => {@link LocalPkgInfo}[], <br/> |
| * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}</td> |
| * </tr> |
| * |
| * <tr> |
| * <td>System images</td> |
| * <td>{@link AndroidVersion} x {@link String} ABI</td> |
| * <td>{@code getPkgsInfos(PkgType.PKG_SYS_IMAGES)} => {@link LocalPkgInfo}[]</td> |
| * </tr> |
| * |
| * </table> |
| * |
| * Apps/libraries that use it are encouraged to keep an existing instance around |
| * (using a singleton or similar mechanism). |
| * <p/> |
| * Threading: All accessor methods are synchronized on the same internal lock so |
| * it's safe to call them from any thread, even concurrently. <br/> |
| * A method like {@code getPkgsInfos} returns a copy of its data array, which objects are |
| * not altered after creation, so its value is not influenced by the internal state after |
| * it returns. |
| * <p/> |
| * |
| * Implementation Background: |
| * <ul> |
| * <li> The sdk manager has a set of "Package" classes that cover both local |
| * and remote SDK operations. |
| * <li> Goal was to split it in 2 cleanly separated parts: {@link LocalSdk} parses sdk on disk, |
| * and {@link RemoteSdk} wraps the downloaded manifest. |
| * <li> The local SDK should be a singleton accessible somewhere, so there will be one in ADT |
| * (via the Sdk instance), one in Studio, and one in the command line tool. <br/> |
| * Right now there's a bit of mess with some classes creating a temp LocalSdkParser, |
| * some others using an SdkManager instance, and that needs to be sorted out. |
| * <li> As a transition, the SdkManager instance wraps a LocalSdk and uses this. Eventually the |
| * SdkManager.java class will go away (its name is totally misleading, for starters.) |
| * <li> The current LocalSdkParser stays as-is for compatibility purposes and the goal is also |
| * to totally remove it when the SdkManager class goes away. |
| * </ul> |
| * @version 2 of the {@code SdkManager} class, essentially. |
| */ |
| public class LocalSdk { |
| |
| /** Location of the SDK. Maybe null. Can be changed. */ |
| private File mSdkRoot; |
| /** File operation object. (Used for overriding in mock testing.) */ |
| private final IFileOp mFileOp; |
| /** List of package information loaded so far. Lazily populated. */ |
| @GuardedBy(value="mLocalPackages") |
| private final Multimap<PkgType, LocalPkgInfo> mLocalPackages = TreeMultimap.create(); |
| /** Directories already parsed into {@link #mLocalPackages}. */ |
| @GuardedBy(value="mLocalPackages") |
| private final Multimap<PkgType, LocalDirInfo> mVisitedDirs = HashMultimap.create(); |
| /** A legacy build-tool for older platform-tools < 17. */ |
| private BuildToolInfo mLegacyBuildTools; |
| /** Cache of targets from local sdk. See {@link #getTargets()}. */ |
| @GuardedBy(value="mLocalPackages") |
| private final List<IAndroidTarget> mCachedTargets = new ArrayList<IAndroidTarget>(); |
| private boolean mReloadTargets = true; |
| |
| /** |
| * Creates an initial LocalSdk instance with an unknown location. |
| */ |
| public LocalSdk() { |
| mFileOp = new FileOp(); |
| } |
| |
| /** |
| * Creates an initial LocalSdk instance for a known SDK location. |
| * |
| * @param sdkRoot The location of the SDK root folder. |
| */ |
| public LocalSdk(@NonNull File sdkRoot) { |
| this(); |
| setLocation(sdkRoot); |
| } |
| |
| /** |
| * Creates an initial LocalSdk instance with an unknown location. |
| * This is designed for unit tests to override the {@link FileOp} being used. |
| * |
| * @param fileOp The alternate {@link FileOp} to use for all file-based interactions. |
| */ |
| @VisibleForTesting(visibility=Visibility.PRIVATE) |
| protected LocalSdk(@NonNull IFileOp fileOp) { |
| mFileOp = fileOp; |
| } |
| |
| /* |
| * Returns the current IFileOp being used. |
| */ |
| @NonNull |
| public IFileOp getFileOp() { |
| return mFileOp; |
| } |
| |
| /** |
| * Sets or changes the SDK root location. This also clears any cached information. |
| * |
| * @param sdkRoot The location of the SDK root folder. |
| */ |
| public void setLocation(@NonNull File sdkRoot) { |
| assert sdkRoot != null; |
| mSdkRoot = sdkRoot; |
| clearLocalPkg(PkgType.PKG_ALL); |
| } |
| |
| /** |
| * Location of the SDK. Maybe null. Can be changed. |
| * |
| * @return The location of the SDK. Null if not initialized yet. |
| */ |
| @Nullable |
| public File getLocation() { |
| return mSdkRoot; |
| } |
| |
| /** |
| * Location of the SDK. Maybe null. Can be changed. |
| * The getLocation() API replaces this function. |
| * @return The location of the SDK. Null if not initialized yet. |
| */ |
| @Deprecated |
| @Nullable |
| public String getPath() { |
| return mSdkRoot != null ? mSdkRoot.getPath() : null; |
| } |
| |
| /** |
| * Clear the tracked visited folders & the cached {@link LocalPkgInfo} for the |
| * given filter types. |
| * |
| * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. |
| */ |
| public void clearLocalPkg(@NonNull EnumSet<PkgType> filters) { |
| mLegacyBuildTools = null; |
| |
| synchronized (mLocalPackages) { |
| for (PkgType filter : filters) { |
| mVisitedDirs.removeAll(filter); |
| mLocalPackages.removeAll(filter); |
| } |
| } |
| |
| // Clear the targets if the platforms or addons are being cleared |
| if (filters.contains(PkgType.PKG_PLATFORM) || filters.contains(PkgType.PKG_ADDON)) { |
| mReloadTargets = true; |
| } |
| } |
| |
| /** |
| * Check the tracked visited folders to see if anything has changed for the |
| * requested filter types. |
| * This does not refresh or reload any package information. |
| * |
| * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. |
| */ |
| public boolean hasChanged(@NonNull EnumSet<PkgType> filters) { |
| for (PkgType filter : filters) { |
| Collection<LocalDirInfo> dirInfos; |
| synchronized (mLocalPackages) { |
| dirInfos = mVisitedDirs.get(filter); |
| } |
| for(LocalDirInfo dirInfo : dirInfos) { |
| if (dirInfo.hasChanged()) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| //--------- Generic querying --------- |
| |
| |
| /** |
| * Retrieves information on a package identified by an {@link IPkgDesc}. |
| * |
| * @param descriptor {@link IPkgDesc} describing a package. |
| * @return The first package found with the same descriptor or null. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull IPkgDesc descriptor) { |
| |
| for (LocalPkgInfo pkg : getPkgsInfos(EnumSet.of(descriptor.getType()))) { |
| IPkgDesc d = pkg.getDesc(); |
| if (d.equals(descriptor)) { |
| return pkg; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Retrieves information on a package identified by an {@link AndroidVersion}. |
| * |
| * Note: don't use this for {@link PkgType#PKG_SYS_IMAGE} since there can be more than |
| * one ABI and this method only returns a single package per filter type. |
| * |
| * @param filter {@link PkgType#PKG_PLATFORM}, {@link PkgType#PKG_SAMPLE} |
| * or {@link PkgType#PKG_SOURCE}. |
| * @param version The {@link AndroidVersion} specific for this package type. |
| * @return An existing package information or null if not found. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull AndroidVersion version) { |
| assert filter == PkgType.PKG_PLATFORM || |
| filter == PkgType.PKG_SAMPLE || |
| filter == PkgType.PKG_SOURCE; |
| |
| for (LocalPkgInfo pkg : getPkgsInfos(filter)) { |
| IPkgDesc d = pkg.getDesc(); |
| if (d.hasAndroidVersion() && d.getAndroidVersion().equals(version)) { |
| return pkg; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Retrieves information on a package identified by its {@link FullRevision}. |
| * <p/> |
| * Note that {@link PkgType#PKG_TOOLS} and {@link PkgType#PKG_PLATFORM_TOOLS} |
| * are unique in a local SDK so you'll want to use {@link #getPkgInfo(PkgType)} |
| * to retrieve them instead. |
| * |
| * @param filter {@link PkgType#PKG_BUILD_TOOLS}. |
| * @param revision The {@link FullRevision} uniquely identifying this package. |
| * @return An existing package information or null if not found. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull FullRevision revision) { |
| |
| assert filter == PkgType.PKG_BUILD_TOOLS; |
| |
| for (LocalPkgInfo pkg : getPkgsInfos(filter)) { |
| IPkgDesc d = pkg.getDesc(); |
| if (d.hasFullRevision() && d.getFullRevision().equals(revision)) { |
| return pkg; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Retrieves information on a package identified by its {@link String} path. |
| * <p/> |
| * For add-ons and platforms, the path is the target hash string |
| * (see {@link AndroidTargetHash} for helpers methods to generate this string.) |
| * |
| * @param filter {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}. |
| * @param path The vendor/path uniquely identifying this package. |
| * @return An existing package information or null if not found. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull String path) { |
| |
| assert filter == PkgType.PKG_ADDON || |
| filter == PkgType.PKG_PLATFORM; |
| |
| for (LocalPkgInfo pkg : getPkgsInfos(filter)) { |
| IPkgDesc d = pkg.getDesc(); |
| if (d.hasPath() && path.equals(d.getPath())) { |
| return pkg; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Retrieves information on a package identified by both vendor and path strings. |
| * <p/> |
| * For add-ons the path is target hash string |
| * (see {@link AndroidTargetHash} for helpers methods to generate this string.) |
| * |
| * @param filter {@link PkgType#PKG_EXTRA}, {@link PkgType#PKG_ADDON}. |
| * @param vendor The vendor id of the extra package. |
| * @param path The path uniquely identifying this package for its vendor. |
| * @return An existing package information or null if not found. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, |
| @NonNull String vendor, |
| @NonNull String path) { |
| |
| assert filter == PkgType.PKG_EXTRA || |
| filter == PkgType.PKG_ADDON; |
| |
| for (LocalPkgInfo pkg : getPkgsInfos(filter)) { |
| IPkgDesc d = pkg.getDesc(); |
| if (d.hasVendor() && vendor.equals(d.getVendor().getId())) { |
| if (d.hasPath() && path.equals(d.getPath())) { |
| return pkg; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Retrieves information on an extra package identified by its {@link String} vendor/path. |
| * |
| * @param vendor The vendor id of the extra package. |
| * @param path The path uniquely identifying this package for its vendor. |
| * @return An existing extra package information or null if not found. |
| */ |
| @Nullable |
| public LocalExtraPkgInfo getExtra(@NonNull String vendor, @NonNull String path) { |
| return (LocalExtraPkgInfo) getPkgInfo(PkgType.PKG_EXTRA, vendor, path); |
| } |
| |
| /** |
| * For unique local packages. |
| * Returns the cached LocalPkgInfo for the requested type. |
| * Loads it from disk if not cached. |
| * |
| * @param filter {@link PkgType#PKG_TOOLS} or {@link PkgType#PKG_PLATFORM_TOOLS} |
| * or {@link PkgType#PKG_DOC}. |
| * @return null if the package is not installed. |
| */ |
| @Nullable |
| public LocalPkgInfo getPkgInfo(@NonNull PkgType filter) { |
| |
| assert filter == PkgType.PKG_TOOLS || |
| filter == PkgType.PKG_PLATFORM_TOOLS || |
| filter == PkgType.PKG_DOC; |
| |
| if (filter != PkgType.PKG_TOOLS && |
| filter != PkgType.PKG_PLATFORM_TOOLS && |
| filter != PkgType.PKG_DOC) { |
| return null; |
| } |
| |
| LocalPkgInfo info = null; |
| synchronized (mLocalPackages) { |
| Collection<LocalPkgInfo> existing = mLocalPackages.get(filter); |
| assert existing.size() <= 1; |
| if (existing.size() > 0) { |
| return existing.iterator().next(); |
| } |
| |
| File uniqueDir = new File(mSdkRoot, filter.getFolderName()); |
| |
| if (!mVisitedDirs.containsEntry(filter, uniqueDir)) { |
| switch(filter) { |
| case PKG_TOOLS: |
| info = scanTools(uniqueDir); |
| break; |
| case PKG_PLATFORM_TOOLS: |
| info = scanPlatformTools(uniqueDir); |
| break; |
| case PKG_DOC: |
| info = scanDoc(uniqueDir); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // Whether we have found a valid pkg or not, this directory has been visited. |
| mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, uniqueDir)); |
| |
| if (info != null) { |
| mLocalPackages.put(filter, info); |
| } |
| } |
| |
| return info; |
| } |
| |
| /** |
| * Retrieve all the info about the requested package type. |
| * This is used for the package types that have one or more instances, each with different |
| * versions. |
| * The resulting array is sorted according to the PkgInfo's sort order. |
| * <p/> |
| * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and |
| * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is |
| * more efficient to use {@link #getPkgInfo(PkgType)} to query them. |
| * |
| * @param filter One of {@link PkgType} constants. |
| * @return A list (possibly empty) of matching installed packages. Never returns null. |
| */ |
| @NonNull |
| public LocalPkgInfo[] getPkgsInfos(@NonNull PkgType filter) { |
| return getPkgsInfos(EnumSet.of(filter)); |
| } |
| |
| /** |
| * Retrieve all the info about the requested package types. |
| * This is used for the package types that have one or more instances, each with different |
| * versions. |
| * The resulting array is sorted according to the PkgInfo's sort order. |
| * <p/> |
| * To force the LocalSdk parser to load <b>everything</b>, simply call this method |
| * with the {@link PkgType#PKG_ALL} argument to load all the known package types. |
| * <p/> |
| * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and |
| * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is |
| * more efficient to use {@link #getPkgInfo(PkgType)} to query them. |
| * |
| * @param filters One or more of {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}, |
| * {@link PkgType#PKG_BUILD_TOOLS}, {@link PkgType#PKG_EXTRA}, |
| * {@link PkgType#PKG_SOURCE}, {@link PkgType#PKG_SYS_IMAGE} |
| * @return A list (possibly empty) of matching installed packages. Never returns null. |
| */ |
| @NonNull |
| public LocalPkgInfo[] getPkgsInfos(@NonNull EnumSet<PkgType> filters) { |
| List<LocalPkgInfo> list = Lists.newArrayList(); |
| |
| for (PkgType filter : filters) { |
| if (filter == PkgType.PKG_TOOLS || |
| filter == PkgType.PKG_PLATFORM_TOOLS || |
| filter == PkgType.PKG_DOC) { |
| LocalPkgInfo info = getPkgInfo(filter); |
| if (info != null) { |
| list.add(info); |
| } |
| } else { |
| synchronized (mLocalPackages) { |
| Collection<LocalPkgInfo> existing = mLocalPackages.get(filter); |
| assert existing != null; // Multimap returns an empty set if not found |
| |
| if (!existing.isEmpty()) { |
| list.addAll(existing); |
| continue; |
| } |
| |
| File subDir = new File(mSdkRoot, filter.getFolderName()); |
| |
| if (!mVisitedDirs.containsEntry(filter, subDir)) { |
| switch(filter) { |
| case PKG_BUILD_TOOLS: |
| scanBuildTools(subDir, existing); |
| break; |
| |
| case PKG_PLATFORM: |
| scanPlatforms(subDir, existing); |
| break; |
| |
| case PKG_SYS_IMAGE: |
| scanSysImages(subDir, existing, false); |
| break; |
| |
| case PKG_ADDON_SYS_IMAGE: |
| scanSysImages(subDir, existing, true); |
| break; |
| |
| case PKG_ADDON: |
| scanAddons(subDir, existing); |
| break; |
| |
| case PKG_SAMPLE: |
| scanSamples(subDir, existing); |
| break; |
| |
| case PKG_SOURCE: |
| scanSources(subDir, existing); |
| break; |
| |
| case PKG_EXTRA: |
| scanExtras(subDir, existing); |
| break; |
| |
| case PKG_TOOLS: |
| case PKG_PLATFORM_TOOLS: |
| case PKG_DOC: |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "Unsupported pkg type " + filter.toString()); |
| } |
| mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, subDir)); |
| list.addAll(existing); |
| } |
| } |
| } |
| } |
| |
| Collections.sort(list); |
| return list.toArray(new LocalPkgInfo[list.size()]); |
| } |
| |
| //---------- Package-specific querying -------- |
| |
| /** |
| * Returns the {@link BuildToolInfo} for the given revision. |
| * |
| * @param revision The requested revision. |
| * @return A {@link BuildToolInfo}. Can be null if {@code revision} is null or is |
| * not part of the known set returned by {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)}. |
| */ |
| @Nullable |
| public BuildToolInfo getBuildTool(@Nullable FullRevision revision) { |
| LocalPkgInfo pkg = getPkgInfo(PkgType.PKG_BUILD_TOOLS, revision); |
| if (pkg instanceof LocalBuildToolPkgInfo) { |
| return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the highest build-tool revision known, or null if there are are no build-tools. |
| * <p/> |
| * If no specific build-tool package is installed but the platform-tools is lower than 17, |
| * then this creates and returns a "legacy" built-tool package using platform-tools. |
| * (We only split build-tools out of platform-tools starting with revision 17, |
| * before they were both the same thing.) |
| * |
| * @return The highest build-tool revision known, or null. |
| */ |
| @Nullable |
| public BuildToolInfo getLatestBuildTool() { |
| if (mLegacyBuildTools != null) { |
| return mLegacyBuildTools; |
| } |
| |
| LocalPkgInfo[] pkgs = getPkgsInfos(PkgType.PKG_BUILD_TOOLS); |
| |
| if (pkgs.length == 0) { |
| LocalPkgInfo ptPkg = getPkgInfo(PkgType.PKG_PLATFORM_TOOLS); |
| if (ptPkg instanceof LocalPlatformToolPkgInfo && |
| ptPkg.getDesc().getFullRevision().compareTo(new FullRevision(17)) < 0) { |
| // older SDK, create a compatible build-tools |
| mLegacyBuildTools = createLegacyBuildTools((LocalPlatformToolPkgInfo) ptPkg); |
| return mLegacyBuildTools; |
| } |
| return null; |
| } |
| |
| assert pkgs.length > 0; |
| |
| // Note: the pkgs come from a TreeMultimap so they should already be sorted. |
| // Just in case, sort them again. |
| Arrays.sort(pkgs); |
| |
| // LocalBuildToolPkgInfo's comparator sorts on its FullRevision so we just |
| // need to take the latest element. |
| LocalPkgInfo pkg = pkgs[pkgs.length - 1]; |
| if (pkg instanceof LocalBuildToolPkgInfo) { |
| return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); |
| } |
| |
| return null; |
| } |
| |
| @NonNull |
| private BuildToolInfo createLegacyBuildTools(@NonNull LocalPlatformToolPkgInfo ptInfo) { |
| File platformTools = new File(getLocation(), SdkConstants.FD_PLATFORM_TOOLS); |
| File platformToolsLib = ptInfo.getLocalDir(); |
| File platformToolsRs = new File(platformTools, SdkConstants.FN_FRAMEWORK_RENDERSCRIPT); |
| |
| return new BuildToolInfo( |
| ptInfo.getDesc().getFullRevision(), |
| platformTools, |
| new File(platformTools, SdkConstants.FN_AAPT), |
| new File(platformTools, SdkConstants.FN_AIDL), |
| new File(platformTools, SdkConstants.FN_DX), |
| new File(platformToolsLib, SdkConstants.FN_DX_JAR), |
| new File(platformTools, SdkConstants.FN_RENDERSCRIPT), |
| new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE), |
| new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE_CLANG), |
| null, |
| null, |
| null, |
| null, |
| new File(platformTools, SdkConstants.FN_ZIPALIGN)); |
| } |
| |
| /** |
| * Returns the targets (platforms & addons) that are available in the SDK. |
| * The target list is created on demand the first time then cached. |
| * It will not refreshed unless {@link #clearLocalPkg} is called to clear platforms |
| * and/or add-ons. |
| * <p/> |
| * The array can be empty but not null. |
| */ |
| @NonNull |
| public IAndroidTarget[] getTargets() { |
| synchronized (mLocalPackages) { |
| if (mReloadTargets) { |
| LocalPkgInfo[] pkgsInfos = getPkgsInfos(EnumSet.of(PkgType.PKG_PLATFORM, |
| PkgType.PKG_ADDON)); |
| int n = pkgsInfos.length; |
| mCachedTargets.clear(); |
| for (int i = 0; i < n; i++) { |
| LocalPkgInfo info = pkgsInfos[i]; |
| assert info instanceof LocalPlatformPkgInfo; |
| if (info instanceof LocalPlatformPkgInfo) { |
| IAndroidTarget target = ((LocalPlatformPkgInfo) info).getAndroidTarget(); |
| if (target != null) { |
| mCachedTargets.add(target); |
| } |
| } |
| } |
| } |
| return mCachedTargets.toArray(new IAndroidTarget[mCachedTargets.size()]); |
| } |
| } |
| |
| /** |
| * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. |
| * |
| * @param hash the {@link IAndroidTarget} hash string. |
| * @return The matching {@link IAndroidTarget} or null. |
| */ |
| @Nullable |
| public IAndroidTarget getTargetFromHashString(@Nullable String hash) { |
| if (hash != null) { |
| IAndroidTarget[] targets = getTargets(); |
| for (IAndroidTarget target : targets) { |
| if (target != null && hash.equals(AndroidTargetHash.getTargetHashString(target))) { |
| return target; |
| } |
| } |
| } |
| return null; |
| } |
| |
| // ------------- |
| |
| /** |
| * Try to find a tools package at the given location. |
| * Returns null if not found. |
| */ |
| private LocalToolPkgInfo scanTools(File toolFolder) { |
| // Can we find some properties? |
| Properties props = parseProperties(new File(toolFolder, SdkConstants.FN_SOURCE_PROP)); |
| FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| return null; |
| } |
| |
| FullRevision minPlatToolsRev = |
| PackageParserUtils.getPropertyFull(props, PkgProps.MIN_PLATFORM_TOOLS_REV); |
| if (minPlatToolsRev == null) { |
| minPlatToolsRev = FullRevision.NOT_SPECIFIED; |
| } |
| |
| LocalToolPkgInfo info = new LocalToolPkgInfo(this, toolFolder, props, rev, minPlatToolsRev); |
| |
| // We're not going to check that all tools are present. At the very least |
| // we should expect to find android and an emulator adapted to the current OS. |
| boolean hasEmulator = false; |
| boolean hasAndroid = false; |
| String android1 = SdkConstants.androidCmdName().replace(".bat", ".exe"); |
| String android2 = android1.indexOf('.') == -1 ? null : android1.replace(".exe", ".bat"); |
| File[] files = mFileOp.listFiles(toolFolder); |
| for (File file : files) { |
| String name = file.getName(); |
| if (SdkConstants.FN_EMULATOR.equals(name)) { |
| hasEmulator = true; |
| } |
| if (android1.equals(name) || (android2 != null && android2.equals(name))) { |
| hasAndroid = true; |
| } |
| } |
| if (!hasAndroid) { |
| info.appendLoadError("Missing %1$s", SdkConstants.androidCmdName()); |
| } |
| if (!hasEmulator) { |
| info.appendLoadError("Missing %1$s", SdkConstants.FN_EMULATOR); |
| } |
| |
| return info; |
| } |
| |
| /** |
| * Try to find a platform-tools package at the given location. |
| * Returns null if not found. |
| */ |
| private LocalPlatformToolPkgInfo scanPlatformTools(File ptFolder) { |
| // Can we find some properties? |
| Properties props = parseProperties(new File(ptFolder, SdkConstants.FN_SOURCE_PROP)); |
| FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| return null; |
| } |
| |
| LocalPlatformToolPkgInfo info = new LocalPlatformToolPkgInfo(this, ptFolder, props, rev); |
| return info; |
| } |
| |
| /** |
| * Try to find a docs package at the given location. |
| * Returns null if not found. |
| */ |
| private LocalDocPkgInfo scanDoc(File docFolder) { |
| // Can we find some properties? |
| Properties props = parseProperties(new File(docFolder, SdkConstants.FN_SOURCE_PROP)); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| return null; |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| LocalDocPkgInfo info = new LocalDocPkgInfo(this, docFolder, props, vers, rev); |
| |
| // To start with, a doc folder should have an "index.html" to be acceptable. |
| // We don't actually check the content of the file. |
| if (!mFileOp.isFile(new File(docFolder, "index.html"))) { |
| info.appendLoadError("Missing index.html"); |
| } |
| return info; |
| |
| } catch (AndroidVersionException e) { |
| return null; // skip invalid or missing android version. |
| } |
| } |
| |
| /** |
| * Helper used by scanXyz methods below to check whether a directory should be visited. |
| * It can be skipped if it's not a directory or if it's already marked as visited in |
| * mVisitedDirs for the given package type -- in which case the directory is added to |
| * the visited map. |
| * |
| * @param pkgType The package type being scanned. |
| * @param directory The file or directory to check. |
| * @return False if directory can/should be skipped. |
| * True if directory should be visited, in which case it's registered in mVisitedDirs. |
| */ |
| private boolean shouldVisitDir(@NonNull PkgType pkgType, @NonNull File directory) { |
| if (!mFileOp.isDirectory(directory)) { |
| return false; |
| } |
| synchronized (mLocalPackages) { |
| if (mVisitedDirs.containsEntry(pkgType, directory)) { |
| return false; |
| } |
| mVisitedDirs.put(pkgType, new LocalDirInfo(mFileOp, directory)); |
| } |
| return true; |
| } |
| |
| private void scanBuildTools(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| // The build-tool root folder contains a list of per-revision folders. |
| for (File buildToolDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_BUILD_TOOLS, buildToolDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(buildToolDir, SdkConstants.FN_SOURCE_PROP)); |
| FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| BuildToolInfo btInfo = new BuildToolInfo(rev, buildToolDir); |
| LocalBuildToolPkgInfo pkgInfo = |
| new LocalBuildToolPkgInfo(this, buildToolDir, props, rev, btInfo); |
| outCollection.add(pkgInfo); |
| } |
| } |
| |
| private void scanPlatforms(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| for (File platformDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_PLATFORM, platformDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| FullRevision minToolsRev = |
| PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); |
| if (minToolsRev == null) { |
| minToolsRev = FullRevision.NOT_SPECIFIED; |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| |
| LocalPlatformPkgInfo pkgInfo = |
| new LocalPlatformPkgInfo(this, platformDir, props, vers, rev, minToolsRev); |
| outCollection.add(pkgInfo); |
| |
| } catch (AndroidVersionException e) { |
| continue; // skip invalid or missing android version. |
| } |
| } |
| } |
| |
| private void scanAddons(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| for (File addonDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_ADDON, addonDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(addonDir, SdkConstants.FN_SOURCE_PROP)); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| |
| // Starting with addon-4.xsd, we have vendor-id and name-id available |
| // in the add-on source properties so we'll use that directly. |
| |
| String nameId = props.getProperty(PkgProps.ADDON_NAME_ID); |
| String nameDisp = props.getProperty(PkgProps.ADDON_NAME_DISPLAY); |
| String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID); |
| String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY); |
| |
| if (nameId == null) { |
| // Support earlier add-ons that only had a name display attribute |
| nameDisp = props.getProperty(PkgProps.ADDON_NAME, "Unknown"); |
| nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(nameDisp); |
| } |
| |
| if (nameId != null && nameDisp == null) { |
| nameDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); |
| } |
| |
| if (vendorId != null && vendorDisp == null) { |
| vendorDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); |
| } |
| |
| if (vendorId == null) { |
| // Support earlier add-ons that only had a vendor display attribute |
| vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR, "Unknown"); |
| vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(vendorDisp); |
| } |
| |
| LocalAddonPkgInfo pkgInfo = new LocalAddonPkgInfo( |
| this, addonDir, props, vers, rev, |
| new IdDisplay(vendorId, vendorDisp), |
| new IdDisplay(nameId, nameDisp)); |
| outCollection.add(pkgInfo); |
| |
| } catch (AndroidVersionException e) { |
| continue; // skip invalid or missing android version. |
| } |
| } |
| } |
| |
| private void scanSysImages( |
| File collectionDir, |
| Collection<LocalPkgInfo> outCollection, |
| boolean scanAddons) { |
| List<File> propFiles = Lists.newArrayList(); |
| |
| // Create a list of folders that contains a source.properties file matching these pattenrs: |
| // sys-img/target/tag/abi |
| // sys-img/target/abis |
| // sys-img/add-on-target/abi |
| for (File platformDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_SYS_IMAGE, platformDir)) { |
| continue; |
| } |
| |
| for (File dir1 : mFileOp.listFiles(platformDir)) { |
| // dir1 might be either a tag or an abi folder. |
| if (!shouldVisitDir(PkgType.PKG_SYS_IMAGE, dir1)) { |
| continue; |
| } |
| |
| File prop1 = new File(dir1, SdkConstants.FN_SOURCE_PROP); |
| if (mFileOp.isFile(prop1)) { |
| // dir1 was a legacy abi folder. |
| if (!propFiles.contains(prop1)) { |
| propFiles.add(prop1); |
| } |
| } else { |
| File[] dir1Files = mFileOp.listFiles(dir1); |
| for (File dir2 : dir1Files) { |
| // dir2 should be an abi folder in a tag folder. |
| if (!shouldVisitDir(PkgType.PKG_SYS_IMAGE, dir2)) { |
| continue; |
| } |
| |
| File prop2 = new File(dir2, SdkConstants.FN_SOURCE_PROP); |
| if (mFileOp.isFile(prop2)) { |
| if (!propFiles.contains(prop2)) { |
| propFiles.add(prop2); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| for (File propFile : propFiles) { |
| Properties props = parseProperties(propFile); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| |
| IdDisplay tag = LocalSysImgPkgInfo.extractTagFromProps(props); |
| String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID, null); |
| File abiDir = propFile.getParentFile(); |
| |
| if (vendorId == null && !scanAddons) { |
| LocalSysImgPkgInfo pkgInfo = |
| new LocalSysImgPkgInfo(this, abiDir, props, vers, tag, abiDir.getName(), rev); |
| outCollection.add(pkgInfo); |
| |
| } else if (vendorId != null && scanAddons) { |
| String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY, vendorId); |
| IdDisplay vendor = new IdDisplay(vendorId, vendorDisp); |
| |
| LocalAddonSysImgPkgInfo pkgInfo = |
| new LocalAddonSysImgPkgInfo( |
| this, abiDir, props, vers, vendor, tag, abiDir.getName(), rev); |
| outCollection.add(pkgInfo); |
| } |
| |
| } catch (AndroidVersionException e) { |
| continue; // skip invalid or missing android version. |
| } |
| } |
| } |
| |
| private void scanSamples(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| for (File platformDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_SAMPLE, platformDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| FullRevision minToolsRev = |
| PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); |
| if (minToolsRev == null) { |
| minToolsRev = FullRevision.NOT_SPECIFIED; |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| |
| LocalSamplePkgInfo pkgInfo = |
| new LocalSamplePkgInfo(this, platformDir, props, vers, rev, minToolsRev); |
| outCollection.add(pkgInfo); |
| } catch (AndroidVersionException e) { |
| continue; // skip invalid or missing android version. |
| } |
| } |
| } |
| |
| private void scanSources(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| // The build-tool root folder contains a list of per-revision folders. |
| for (File platformDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_SOURCE, platformDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); |
| MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| try { |
| AndroidVersion vers = new AndroidVersion(props); |
| |
| LocalSourcePkgInfo pkgInfo = |
| new LocalSourcePkgInfo(this, platformDir, props, vers, rev); |
| outCollection.add(pkgInfo); |
| } catch (AndroidVersionException e) { |
| continue; // skip invalid or missing android version. |
| } |
| } |
| } |
| |
| private void scanExtras(File collectionDir, Collection<LocalPkgInfo> outCollection) { |
| for (File vendorDir : mFileOp.listFiles(collectionDir)) { |
| if (!shouldVisitDir(PkgType.PKG_EXTRA, vendorDir)) { |
| continue; |
| } |
| |
| for (File extraDir : mFileOp.listFiles(vendorDir)) { |
| if (!shouldVisitDir(PkgType.PKG_EXTRA, extraDir)) { |
| continue; |
| } |
| |
| Properties props = parseProperties(new File(extraDir, SdkConstants.FN_SOURCE_PROP)); |
| NoPreviewRevision rev = |
| PackageParserUtils.getPropertyNoPreview(props, PkgProps.PKG_REVISION); |
| if (rev == null) { |
| continue; // skip, no revision |
| } |
| |
| String oldPaths = |
| PackageParserUtils.getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); |
| |
| String vendorId = vendorDir.getName(); |
| String vendorDisp = props.getProperty(PkgProps.EXTRA_VENDOR_DISPLAY); |
| if (vendorDisp == null || vendorDisp.isEmpty()) { |
| vendorDisp = vendorId; |
| } |
| |
| String displayName = props.getProperty(PkgProps.EXTRA_NAME_DISPLAY, null); |
| |
| LocalExtraPkgInfo pkgInfo = new LocalExtraPkgInfo( |
| this, |
| extraDir, |
| props, |
| new IdDisplay(vendorId, vendorDisp), |
| extraDir.getName(), |
| displayName, |
| PkgDescExtra.convertOldPaths(oldPaths), |
| rev); |
| outCollection.add(pkgInfo); |
| } |
| } |
| } |
| |
| /** |
| * Parses the given file as properties file if it exists. |
| * Returns null if the file does not exist, cannot be parsed or has no properties. |
| */ |
| private Properties parseProperties(File propsFile) { |
| InputStream fis = null; |
| try { |
| if (mFileOp.exists(propsFile)) { |
| fis = mFileOp.newFileInputStream(propsFile); |
| |
| Properties props = new Properties(); |
| props.load(fis); |
| |
| // To be valid, there must be at least one property in it. |
| if (props.size() > 0) { |
| return props; |
| } |
| } |
| } catch (IOException e) { |
| // Ignore |
| } finally { |
| if (fis != null) { |
| try { |
| fis.close(); |
| } catch (IOException e) {} |
| } |
| } |
| return null; |
| } |
| |
| } |