FileDocCategorySizeDatePackage
SdkManager.javaAPI DocAndroid 1.5 API19862Wed May 06 22:41:10 BST 2009com.android.sdklib

SdkManager.java

/*
 * Copyright (C) 2008 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;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The SDK manager parses the SDK folder and gives access to the content.
 * @see PlatformTarget
 * @see AddOnTarget 
 */
public final class SdkManager {
    
    public final static String PROP_VERSION_SDK = "ro.build.version.sdk";
    public final static String PROP_VERSION_RELEASE = "ro.build.version.release";
    
    private final static String ADDON_NAME = "name";
    private final static String ADDON_VENDOR = "vendor";
    private final static String ADDON_API = "api";
    private final static String ADDON_DESCRIPTION = "description";
    private final static String ADDON_LIBRARIES = "libraries";
    private final static String ADDON_DEFAULT_SKIN = "skin";
    
    private final static Pattern PATTERN_PROP = Pattern.compile(
            "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$");
    
    private final static Pattern PATTERN_LIB_DATA = Pattern.compile(
            "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE);
    
    /** List of items in the platform to check when parsing it. These paths are relative to the
     * platform root folder. */
    private final static String[] sPlatformContentList = new String[] {
        SdkConstants.FN_FRAMEWORK_LIBRARY,
        SdkConstants.FN_FRAMEWORK_AIDL,
        SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT,
        SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL,
        SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX,
        SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR,
    };

    /** the location of the SDK */
    private final String mSdkLocation;
    private IAndroidTarget[] mTargets;

    /**
     * Creates an {@link SdkManager} for a given sdk location.
     * @param sdkLocation the location of the SDK.
     * @param log the ISdkLog object receiving warning/error from the parsing.
     * @return the created {@link SdkManager} or null if the location is not valid.
     */
    public static SdkManager createManager(String sdkLocation, ISdkLog log) {
        try {
            SdkManager manager = new SdkManager(sdkLocation);
            ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
            manager.loadPlatforms(list, log);
            manager.loadAddOns(list, log);
            
            // sort the targets/add-ons
            Collections.sort(list);
            
            manager.setTargets(list.toArray(new IAndroidTarget[list.size()]));
            
            return manager;
        } catch (IllegalArgumentException e) {
            if (log != null) {
                log.error(e, "Error parsing the sdk.");
            }
        }
        
        return null;
    }
    
    /**
     * Returns the location of the SDK.
     */
    public String getLocation() {
        return mSdkLocation;
    }
    
    /**
     * Returns the targets that are available in the SDK.
     */
    public IAndroidTarget[] getTargets() {
        return mTargets;
    }
    
    /**
     * 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.
     */
    public IAndroidTarget getTargetFromHashString(String hash) {
        if (hash != null) {
            for (IAndroidTarget target : mTargets) {
                if (hash.equals(target.hashString())) {
                    return target;
                }
            }
        }

        return null;
    }


    private SdkManager(String sdkLocation) {
        mSdkLocation = sdkLocation;
    }
    
    private void setTargets(IAndroidTarget[] targets) {
        mTargets = targets;
    }

    /**
     * Loads the Platforms from the SDK.
     * @param list the list to fill with the platforms.
     * @param log the ISdkLog object receiving warning/error from the parsing.
     */
    private void loadPlatforms(ArrayList<IAndroidTarget> list, ISdkLog log) {
        File platformFolder = new File(mSdkLocation, SdkConstants.FD_PLATFORMS);
        if (platformFolder.isDirectory()) {
            File[] platforms  = platformFolder.listFiles();
            
            for (File platform : platforms) {
                if (platform.isDirectory()) {
                    PlatformTarget target = loadPlatform(platform, log);
                    if (target != null) {
                        list.add(target);
                    }
                } else if (log != null) {
                    log.warning("Ignoring platform '%1$s', not a folder.", platform.getName());
                }
            }
            
            return;
        }

        String message = null;
        if (platformFolder.exists() == false) {
            message = "%s is missing.";
        } else {
            message = "%s is not a folder.";
        }

        throw new IllegalArgumentException(String.format(message,
                platformFolder.getAbsolutePath()));
    }

    /**
     * Loads a specific Platform at a given location.
     * @param platform the location of the platform.
     * @param log the ISdkLog object receiving warning/error from the parsing.
     */
    private PlatformTarget loadPlatform(File platform, ISdkLog log) {
        File buildProp = new File(platform, SdkConstants.FN_BUILD_PROP);
        
        if (buildProp.isFile()) {
            Map<String, String> map = parsePropertyFile(buildProp, log);
            
            if (map != null) {
                // look for some specific values in the map.
                try {
                    String apiNumber = map.get(PROP_VERSION_SDK);
                    String apiName = map.get(PROP_VERSION_RELEASE);
                    if (apiNumber != null && apiName != null) {
                        // api number and name looks valid, perform a few more checks
                        if (checkPlatformContent(platform, log) == false) {
                            return null;
                        }
                        // create the target.
                        PlatformTarget target = new PlatformTarget(
                                platform.getAbsolutePath(),
                                map,
                                Integer.parseInt(apiNumber),
                                apiName);
                        
                        // need to parse the skins.
                        String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
                        target.setSkins(skins);

                        return target;
                    }
                } catch (NumberFormatException e) {
                    // looks like apiNumber does not parse to a number.
                    // Ignore this platform.
                    if (log != null) {
                        log.error(null,
                                "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.",
                                platform.getName(), PROP_VERSION_SDK, SdkConstants.FN_BUILD_PROP);
                    }
                }
            }
        } else if (log != null) {
            log.error(null, "Ignoring platform '%1$s': %2$s is missing.", platform.getName(),
                    SdkConstants.FN_BUILD_PROP);
        }
        
        return null;
    }

    /**
     * Loads the Add-on from the SDK.
     * @param list the list to fill with the add-ons.
     * @param log the ISdkLog object receiving warning/error from the parsing.
     */
    private void loadAddOns(ArrayList<IAndroidTarget> list, ISdkLog log) {
        File addonFolder = new File(mSdkLocation, SdkConstants.FD_ADDONS);
        if (addonFolder.isDirectory()) {
            File[] addons  = addonFolder.listFiles();
            
            for (File addon : addons) {
                // Add-ons have to be folders. Ignore files and no need to warn about them.
                if (addon.isDirectory()) {
                    AddOnTarget target = loadAddon(addon, list, log);
                    if (target != null) {
                        list.add(target);
                    }
                }
            }

            return;
        }

        String message = null;
        if (addonFolder.exists() == false) {
            message = "%s is missing.";
        } else {
            message = "%s is not a folder.";
        }

        throw new IllegalArgumentException(String.format(message,
                addonFolder.getAbsolutePath()));
    }
    
    /**
     * Loads a specific Add-on at a given location.
     * @param addon the location of the addon.
     * @param list 
     * @param log 
     */
    private AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> list, ISdkLog log) {
        File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI);
        
        if (addOnManifest.isFile()) {
            Map<String, String> propertyMap = parsePropertyFile(addOnManifest, log);
            
            if (propertyMap != null) {
                // look for some specific values in the map.
                // we require name, vendor, and api
                String name = propertyMap.get(ADDON_NAME);
                if (name == null) {
                    displayAddonManifestError(log, addon.getName(), ADDON_NAME);
                    return null;
                }
                
                String vendor = propertyMap.get(ADDON_VENDOR);
                if (vendor == null) {
                    displayAddonManifestError(log, addon.getName(), ADDON_VENDOR);
                    return null;
                }

                String api = propertyMap.get(ADDON_API);
                PlatformTarget baseTarget = null;
                if (api == null) {
                    displayAddonManifestError(log, addon.getName(), ADDON_API);
                    return null;
                } else {
                    try {
                        int apiValue = Integer.parseInt(api);
                        for (IAndroidTarget target : list) {
                            if (target.isPlatform() &&
                                    target.getApiVersionNumber() == apiValue) {
                                baseTarget = (PlatformTarget)target;
                                break;
                            }
                        }
                        
                        if (baseTarget == null) {
                            if (log != null) {
                                log.error(null,
                                        "Ignoring add-on '%1$s': Unable to find base platform with API level %2$d",
                                        addon.getName(), apiValue);
                            }

                            return null;
                        }
                    } catch (NumberFormatException e) {
                        // looks like apiNumber does not parse to a number.
                        // Ignore this add-on.
                        if (log != null) {
                            log.error(null,
                                    "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.",
                                    addon.getName(), ADDON_API, SdkConstants.FN_BUILD_PROP);
                        }
                        return null;
                    }
                }
                
                // get the optional description
                String description = propertyMap.get(ADDON_DESCRIPTION);
                
                // get the optional libraries
                String librariesValue = propertyMap.get(ADDON_LIBRARIES);
                Map<String, String[]> libMap = null;
                
                if (librariesValue != null) {
                    librariesValue = librariesValue.trim();
                    if (librariesValue.length() > 0) {
                        // split in the string into the libraries name
                        String[] libraries = librariesValue.split(";");
                        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 if (log != null) {
                                        log.error(null,
                                                "Ignoring library '%1$s', property value has wrong format\n\t%2$s",
                                                libName, libData);
                                    }
                                } else if (log != null) {
                                    log.error(null,
                                            "Ignoring library '%1$s', missing property value",
                                            libName, libData);
                                }
                            }
                        }
                    }
                }

                AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor,
                        description, libMap, baseTarget);
                
                // need to parse the skins.
                String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
                
                // get the default skin, or take it from the base platform if needed.
                String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN);
                
                if (defaultSkin == null) {
                    if (skins.length == 1) {
                        defaultSkin = skins[1];
                    } else {
                        defaultSkin = baseTarget.getDefaultSkin();
                    }
                }
                
                target.setSkins(skins, defaultSkin);

                return target;
            }
        } else if (log != null) {
            log.error(null, "Ignoring add-on '%1$s': %2$s is missing.", addon.getName(),
                    SdkConstants.FN_MANIFEST_INI);
        }
        
        return null;
    }
    
    private void displayAddonManifestError(ISdkLog log, String addonName, String valueName) {
        if (log != null) {
            log.error(null, "Ignoring add-on '%1$s': '%2$s' is missing from %3$s.",
                    addonName, valueName, SdkConstants.FN_MANIFEST_INI);
        }
    }
    
    /**
     * Checks the given platform has all the required files, and returns true if they are all
     * present.
     * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe),
     * aidl(.exe), dx(.bat), and dx.jar
     */
    private boolean checkPlatformContent(File platform, ISdkLog log) {
        for (String relativePath : sPlatformContentList) {
            File f = new File(platform, relativePath);
            if (f.exists() == false) {
                log.error(null,
                        "Ignoring platform '%1$s': %2$s is missing.",
                        platform.getName(), relativePath);
                return false;
            }
            
        }
        return true;
    }

    
    /**
     * Parses a property file and returns
     * @param buildProp the property file to parse
     * @param log the ISdkLog object receiving warning/error from the parsing.
     * @return the map of (key,value) pairs, or null if the parsing failed.
     */
    public static Map<String, String> parsePropertyFile(File buildProp, ISdkLog log) {
        FileInputStream fis = null;
        BufferedReader reader = null;
        try {
            fis = new FileInputStream(buildProp);
            reader = new BufferedReader(new InputStreamReader(fis));

            String line = null;
            Map<String, String> map = new HashMap<String, String>();
            while ((line = reader.readLine()) != null) {
                if (line.length() > 0 && line.charAt(0) != '#') {
                    
                    Matcher m = PATTERN_PROP.matcher(line);
                    if (m.matches()) {
                        map.put(m.group(1), m.group(2));
                    } else {
                        log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax",
                                buildProp.getAbsolutePath(), line);
                        return null;
                    }
                }
            }
            
            return map;
        } catch (FileNotFoundException e) {
            // this should not happen since we usually test the file existence before
            // calling the method.
            // Return null below.
        } catch (IOException e) {
            if (log != null) {
                log.warning("Error parsing '%1$s': %2$s.", buildProp.getAbsolutePath(),
                        e.getMessage());
            }
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // pass
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    // pass
                }
            }
        }

        return null;
    }
    
    /**
     * Parses the skin folder and builds the skin list.
     * @param osPath The path of the skin root folder.
     */
    private String[] parseSkinFolder(String osPath) {
        File skinRootFolder = new File(osPath);

        if (skinRootFolder.isDirectory()) {
            ArrayList<String> skinList = new ArrayList<String>();

            File[] files = skinRootFolder.listFiles();

            for (File skinFolder : files) {
                if (skinFolder.isDirectory()) {
                    // check for layout file
                    File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT);

                    if (layout.isFile()) {
                        // for now we don't parse the content of the layout and
                        // simply add the directory to the list.
                        skinList.add(skinFolder.getName());
                    }
                }
            }

            return skinList.toArray(new String[skinList.size()]);
        }
        
        return new String[0];
    }
}