| /* |
| * 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.sdklib.AndroidVersion; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.ISystemImage; |
| import com.android.sdklib.ISystemImage.LocationType; |
| import com.android.sdklib.SystemImage; |
| import com.android.sdklib.internal.androidTarget.AddOnTarget; |
| import com.android.sdklib.internal.androidTarget.PlatformTarget; |
| import com.android.sdklib.internal.project.ProjectProperties; |
| import com.android.sdklib.io.FileOp; |
| import com.android.sdklib.io.IFileOp; |
| import com.android.sdklib.repository.AddonManifestIniProps; |
| import com.android.sdklib.repository.FullRevision; |
| import com.android.sdklib.repository.MajorRevision; |
| import com.android.sdklib.repository.descriptors.*; |
| import com.android.utils.Pair; |
| import com.google.common.base.Objects; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.collect.TreeMultimap; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.util.*; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| @SuppressWarnings("MethodMayBeStatic") |
| public class LocalAddonPkgInfo extends LocalPlatformPkgInfo { |
| |
| private static final Pattern PATTERN_LIB_DATA = Pattern.compile( |
| "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ |
| |
| // usb ids are 16-bit hexadecimal values. |
| private static final Pattern PATTERN_USB_IDS = Pattern.compile( |
| "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ |
| |
| @NonNull |
| private final IPkgDescAddon mAddonDesc; |
| |
| public LocalAddonPkgInfo(@NonNull LocalSdk localSdk, |
| @NonNull File localDir, |
| @NonNull Properties sourceProps, |
| @NonNull AndroidVersion version, |
| @NonNull MajorRevision revision, |
| @NonNull IdDisplay vendor, |
| @NonNull IdDisplay name) { |
| super(localSdk, localDir, sourceProps, version, revision, FullRevision.NOT_SPECIFIED); |
| mAddonDesc = (IPkgDescAddon) PkgDesc.Builder.newAddon(version, revision, vendor, name) |
| .create(); |
| } |
| |
| @NonNull |
| @Override |
| public IPkgDesc getDesc() { |
| return mAddonDesc; |
| } |
| |
| /** The "path" of an add-on is its Target Hash. */ |
| @Override |
| @NonNull |
| public String getTargetHash() { |
| return getDesc().getPath(); |
| } |
| |
| //----- |
| |
| /** |
| * Computes a sanitized name-id based on an addon name-display. |
| * This is used to provide compatibility with older add-ons that lacks the new fields. |
| * |
| * @param displayName A name-display field or a old-style name field. |
| * @return A non-null sanitized name-id that fits in the {@code [a-zA-Z0-9_-]+} pattern. |
| */ |
| public static String sanitizeDisplayToNameId(@NonNull String displayName) { |
| String name = displayName.toLowerCase(Locale.US); |
| name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ |
| name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| // Trim leading and trailing underscores |
| if (name.length() > 1) { |
| name = name.replaceAll("^_+", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| if (name.length() > 1) { |
| name = name.replaceAll("_+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| return name; |
| } |
| |
| //----- |
| |
| /** |
| * Creates the AddOnTarget. Invoked by {@link #getAndroidTarget()}. |
| */ |
| @Override |
| @Nullable |
| protected IAndroidTarget createAndroidTarget() { |
| LocalSdk sdk = getLocalSdk(); |
| IFileOp fileOp = sdk.getFileOp(); |
| |
| // Parse the addon properties to ensure we can load it. |
| Pair<Map<String, String>, String> infos = parseAddonProperties(); |
| |
| Map<String, String> propertyMap = infos.getFirst(); |
| String error = infos.getSecond(); |
| |
| if (error != null) { |
| appendLoadError("Ignoring add-on '%1$s': %2$s", getLocalDir().getName(), error); |
| return null; |
| } |
| |
| // Since error==null we're not supposed to encounter any issues loading this add-on. |
| try { |
| assert propertyMap != null; |
| |
| String api = propertyMap.get(AddonManifestIniProps.ADDON_API); |
| String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); |
| String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); |
| |
| assert api != null; |
| assert name != null; |
| assert vendor != null; |
| |
| PlatformTarget baseTarget = null; |
| |
| // Look for a platform that has a matching api level or codename. |
| LocalPkgInfo plat = sdk.getPkgInfo(PkgType.PKG_PLATFORM, |
| getDesc().getAndroidVersion()); |
| if (plat instanceof LocalPlatformPkgInfo) { |
| baseTarget = (PlatformTarget) ((LocalPlatformPkgInfo) plat).getAndroidTarget(); |
| } |
| assert baseTarget != null; |
| |
| // get the optional description |
| String description = propertyMap.get(AddonManifestIniProps.ADDON_DESCRIPTION); |
| |
| // get the add-on revision |
| int revisionValue = 1; |
| String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); |
| if (revision == null) { |
| revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); |
| } |
| if (revision != null) { |
| revisionValue = Integer.parseInt(revision); |
| } |
| |
| // get the optional libraries |
| String librariesValue = propertyMap.get(AddonManifestIniProps.ADDON_LIBRARIES); |
| Map<String, String[]> libMap = null; |
| |
| if (librariesValue != null) { |
| librariesValue = librariesValue.trim(); |
| if (!librariesValue.isEmpty()) { |
| // split in the string into the libraries name |
| String[] libraries = librariesValue.split(";"); //$NON-NLS-1$ |
| if (libraries.length > 0) { |
| libMap = new HashMap<String, String[]>(); |
| for (String libName : libraries) { |
| libName = libName.trim(); |
| |
| // get the library data from the properties |
| String libData = propertyMap.get(libName); |
| |
| if (libData != null) { |
| // split the jar file from the description |
| Matcher m = PATTERN_LIB_DATA.matcher(libData); |
| if (m.matches()) { |
| libMap.put(libName, new String[] { |
| m.group(1), m.group(2) }); |
| } else { |
| appendLoadError( |
| "Ignoring library '%1$s', property value has wrong format\n\t%2$s", |
| libName, libData); |
| } |
| } else { |
| appendLoadError( |
| "Ignoring library '%1$s', missing property value", |
| libName, libData); |
| } |
| } |
| } |
| } |
| } |
| |
| // get the abi list. |
| ISystemImage[] systemImages = getAddonSystemImages(fileOp); |
| |
| // check whether the add-on provides its own rendering info/library. |
| boolean hasRenderingLibrary = false; |
| boolean hasRenderingResources = false; |
| |
| File dataFolder = new File(getLocalDir(), SdkConstants.FD_DATA); |
| if (fileOp.isDirectory(dataFolder)) { |
| hasRenderingLibrary = |
| fileOp.isFile(new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR)); |
| hasRenderingResources = |
| fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_RES)) && |
| fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_FONTS)); |
| } |
| |
| AddOnTarget target = new AddOnTarget( |
| getLocalDir().getAbsolutePath(), |
| name, |
| vendor, |
| revisionValue, |
| description, |
| systemImages, |
| libMap, |
| hasRenderingLibrary, |
| hasRenderingResources, |
| baseTarget); |
| |
| // parse the legacy skins, located under SDK/addons/addon-name/skins/[skin-name] |
| // and merge with the system-image skins, if any, merging them by name. |
| File targetSkinFolder = target.getFile(IAndroidTarget.SKINS); |
| |
| Map<String, File> skinsMap = new TreeMap<String, File>(); |
| |
| for (File f : PackageParserUtils.parseSkinFolder(targetSkinFolder, fileOp)) { |
| skinsMap.put(f.getName().toLowerCase(Locale.US), f); |
| } |
| for (ISystemImage si : systemImages) { |
| for (File f : si.getSkins()) { |
| skinsMap.put(f.getName().toLowerCase(Locale.US), f); |
| } |
| } |
| |
| List<File> skins = new ArrayList<File>(skinsMap.values()); |
| Collections.sort(skins); |
| |
| // get the default skin |
| File defaultSkin = null; |
| String defaultSkinName = propertyMap.get(AddonManifestIniProps.ADDON_DEFAULT_SKIN); |
| if (defaultSkinName != null) { |
| defaultSkin = new File(targetSkinFolder, defaultSkinName); |
| } else { |
| // No default skin name specified, use the first one from the addon |
| // or the default from the platform. |
| if (skins.size() == 1) { |
| defaultSkin = skins.get(0); |
| } else { |
| defaultSkin = baseTarget.getDefaultSkin(); |
| } |
| } |
| |
| // get the USB ID (if available) |
| int usbVendorId = convertId(propertyMap.get(AddonManifestIniProps.ADDON_USB_VENDOR)); |
| if (usbVendorId != IAndroidTarget.NO_USB_ID) { |
| target.setUsbVendorId(usbVendorId); |
| } |
| |
| target.setSkins(skins.toArray(new File[skins.size()]), defaultSkin); |
| |
| return target; |
| |
| } catch (Exception e) { |
| appendLoadError("Ignoring add-on '%1$s': error %2$s.", |
| getLocalDir().getName(), e.toString()); |
| } |
| |
| return null; |
| |
| } |
| |
| /** |
| * Parses the add-on properties and decodes any error that occurs when loading an addon. |
| * |
| * @return A pair with the property map and an error string. Both can be null but not at the |
| * same time. If a non-null error is present then the property map must be ignored. The error |
| * should be translatable as it might show up in the SdkManager UI. |
| */ |
| @NonNull |
| private Pair<Map<String, String>, String> parseAddonProperties() { |
| Map<String, String> propertyMap = null; |
| String error = null; |
| |
| IFileOp fileOp = getLocalSdk().getFileOp(); |
| File addOnManifest = new File(getLocalDir(), SdkConstants.FN_MANIFEST_INI); |
| |
| do { |
| if (!fileOp.isFile(addOnManifest)) { |
| error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI); |
| break; |
| } |
| |
| try { |
| propertyMap = ProjectProperties.parsePropertyStream( |
| fileOp.newFileInputStream(addOnManifest), |
| addOnManifest.getPath(), |
| null /*log*/); |
| if (propertyMap == null) { |
| error = String.format("Failed to parse properties from %1$s", |
| SdkConstants.FN_MANIFEST_INI); |
| break; |
| } |
| } catch (FileNotFoundException e) { |
| // this can happen if the system fails to open the file because of too many |
| // open files. |
| error = String.format("Failed to parse properties from %1$s: %2$s", |
| SdkConstants.FN_MANIFEST_INI, e.getMessage()); |
| break; |
| } |
| |
| // look for some specific values in the map. |
| // we require name, vendor, and api |
| String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); |
| if (name == null) { |
| error = addonManifestWarning(AddonManifestIniProps.ADDON_NAME); |
| break; |
| } |
| |
| String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); |
| if (vendor == null) { |
| error = addonManifestWarning(AddonManifestIniProps.ADDON_VENDOR); |
| break; |
| } |
| |
| String api = propertyMap.get(AddonManifestIniProps.ADDON_API); |
| if (api == null) { |
| error = addonManifestWarning(AddonManifestIniProps.ADDON_API); |
| break; |
| } |
| |
| // Look for a platform that has a matching api level or codename. |
| IAndroidTarget baseTarget = null; |
| LocalPkgInfo plat = getLocalSdk().getPkgInfo(PkgType.PKG_PLATFORM, |
| getDesc().getAndroidVersion()); |
| if (plat instanceof LocalPlatformPkgInfo) { |
| baseTarget = ((LocalPlatformPkgInfo) plat).getAndroidTarget(); |
| } |
| |
| if (baseTarget == null) { |
| error = String.format("Unable to find base platform with API level '%1$s'", api); |
| break; |
| } |
| |
| // get the add-on revision |
| String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); |
| if (revision == null) { |
| revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); |
| } |
| if (revision != null) { |
| try { |
| Integer.parseInt(revision); |
| } catch (NumberFormatException e) { |
| // looks like revision does not parse to a number. |
| error = String.format("%1$s is not a valid number in %2$s.", |
| AddonManifestIniProps.ADDON_REVISION, SdkConstants.FN_BUILD_PROP); |
| break; |
| } |
| } |
| |
| } while(false); |
| |
| return Pair.of(propertyMap, error); |
| } |
| |
| /** |
| * Prepares a warning about the addon being ignored due to a missing manifest value. |
| * This string will show up in the SdkManager UI. |
| * |
| * @param valueName The missing manifest value, for display. |
| */ |
| @NonNull |
| private static String addonManifestWarning(@NonNull String valueName) { |
| return String.format("'%1$s' is missing from %2$s.", |
| valueName, SdkConstants.FN_MANIFEST_INI); |
| } |
| |
| /** |
| * Converts a string representation of an hexadecimal ID into an int. |
| * @param value the string to convert. |
| * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the conversion failed. |
| */ |
| private int convertId(@Nullable String value) { |
| if (value != null && !value.isEmpty()) { |
| if (PATTERN_USB_IDS.matcher(value).matches()) { |
| String v = value.substring(2); |
| try { |
| return Integer.parseInt(v, 16); |
| } catch (NumberFormatException e) { |
| // this shouldn't happen since we check the pattern above, but this is safer. |
| // the method will return 0 below. |
| } |
| } |
| } |
| |
| return IAndroidTarget.NO_USB_ID; |
| } |
| |
| /** |
| * Get all the system images supported by an add-on target. |
| * For an add-on, we first look in the new sdk/system-images folders then we look |
| * for sub-folders in the addon/images directory. |
| * If none are found but the directory exists and is not empty, assume it's a legacy |
| * arm eabi system image. |
| * If any given API appears twice or more, the first occurrence wins. |
| * <p/> |
| * Note that it's OK for an add-on to have no system-images at all, since it can always |
| * rely on the ones from its base platform. |
| * |
| * @param fileOp File operation wrapper. |
| * @return an array of ISystemImage containing all the system images for the target. |
| * The list can be empty but not null. |
| */ |
| @NonNull |
| private ISystemImage[] getAddonSystemImages(IFileOp fileOp) { |
| Set<ISystemImage> found = new TreeSet<ISystemImage>(); |
| SetMultimap<IdDisplay, String> tagToAbiFound = TreeMultimap.create(); |
| |
| |
| // Look in the system images folders: |
| // - SDK/system-image/platform/addon-id-tag/abi |
| // - SDK/system-image/addon-id-tag/abi (many abi possible) |
| // Optional: look for skins under |
| // - SDK/system-image/platform/addon-id-tag/abi/skins/skin-name |
| // - SDK/system-image/addon-id-tag/abi/skins/skin-name |
| // If we find multiple occurrences of the same platform/abi, the first one read wins. |
| |
| LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE); |
| for (LocalPkgInfo pkg : sysImgInfos) { |
| IPkgDesc d = pkg.getDesc(); |
| if (pkg instanceof LocalAddonSysImgPkgInfo && |
| d.hasVendor() && |
| mAddonDesc.getVendor().equals(d.getVendor()) && |
| mAddonDesc.getName().equals(d.getTag()) && |
| Objects.equal(mAddonDesc.getAndroidVersion(), pkg.getDesc().getAndroidVersion())) { |
| final IdDisplay tag = mAddonDesc.getName(); |
| final String abi = d.getPath(); |
| if (abi != null && !tagToAbiFound.containsEntry(tag, abi)) { |
| found.add(((LocalAddonSysImgPkgInfo)pkg).getSystemImage()); |
| tagToAbiFound.put(tag, abi); |
| } |
| } |
| } |
| |
| // Look for sub-directories: |
| // - SDK/addons/addon-name/images/abi (multiple abi possible) |
| // - SDK/addons/addon-name/armeabi (legacy support) |
| boolean useLegacy = true; |
| boolean hasImgFiles = false; |
| final IdDisplay defaultTag = SystemImage.DEFAULT_TAG; |
| |
| File imagesDir = new File(getLocalDir(), SdkConstants.OS_IMAGES_FOLDER); |
| File[] files = fileOp.listFiles(imagesDir); |
| for (File file : files) { |
| if (fileOp.isDirectory(file)) { |
| useLegacy = false; |
| String abi = file.getName(); |
| if (!tagToAbiFound.containsEntry(defaultTag, abi)) { |
| found.add(new SystemImage( |
| file, |
| LocationType.IN_IMAGES_SUBFOLDER, |
| SystemImage.DEFAULT_TAG, |
| mAddonDesc.getVendor(), |
| abi, |
| FileOp.EMPTY_FILE_ARRAY)); |
| tagToAbiFound.put(defaultTag, abi); |
| } |
| } else if (!hasImgFiles && fileOp.isFile(file)) { |
| if (file.getName().endsWith(".img")) { //$NON-NLS-1$ |
| // The legacy images folder is only valid if it contains some .img files |
| hasImgFiles = true; |
| } |
| } |
| } |
| |
| if (useLegacy && |
| hasImgFiles && |
| fileOp.isDirectory(imagesDir) && |
| !tagToAbiFound.containsEntry(defaultTag, SdkConstants.ABI_ARMEABI)) { |
| // We found no sub-folder system images but it looks like the top directory |
| // has some img files in it. It must be a legacy ARM EABI system image folder. |
| found.add(new SystemImage( |
| imagesDir, |
| LocationType.IN_LEGACY_FOLDER, |
| SystemImage.DEFAULT_TAG, |
| SdkConstants.ABI_ARMEABI, |
| FileOp.EMPTY_FILE_ARRAY)); |
| } |
| |
| return found.toArray(new ISystemImage[found.size()]); |
| } |
| } |