/*
* Copyright (C) 2010 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.internal.export;
import com.android.resources.Density;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException;
import com.android.sdklib.internal.project.ApkSettings;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import com.android.sdklib.xml.ManifestData;
import com.android.sdklib.xml.ManifestData.SupportsScreens;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Class representing an Android project and its properties.
*
* Only the properties that pertain to the multi-apk export are present.
*/
public final class ProjectConfig {
private static final String PROP_API = "api";
private static final String PROP_SCREENS = "screens";
private static final String PROP_GL = "gl";
private static final String PROP_ABI = "splitByAbi";
private static final String PROP_DENSITY = "splitByDensity";
private static final String PROP_LOCALEFILTERS = "localeFilters";
/**
* List of densities and their associated aapt filter.
*/
private static final String[][] DENSITY_LIST = new String[][] {
new String[] { Density.HIGH.getResourceValue(),
Density.HIGH.getResourceValue() + "," + Density.NODPI.getResourceValue() },
new String[] { Density.MEDIUM.getResourceValue(),
Density.MEDIUM.getResourceValue() + "," +
Density.NODPI.getResourceValue() },
new String[] { Density.MEDIUM.getResourceValue(),
Density.MEDIUM.getResourceValue() + "," + Density.NODPI.getResourceValue() },
};
private final File mProjectFolder;
private final String mRelativePath;
private final int mMinSdkVersion;
private final int mGlEsVersion;
private final SupportsScreens mSupportsScreens;
private final boolean mSplitByAbi;
private final boolean mSplitByDensity;
private final Map<String, String> mLocaleFilters;
/** List of ABIs not defined in the properties but actually existing in the project as valid
* .so files */
private final List<String> mAbis;
static ProjectConfig create(File projectFolder, String relativePath,
ManifestData manifestData) throws ExportException {
// load the project properties
ProjectProperties projectProp = ProjectProperties.load(projectFolder.getAbsolutePath(),
PropertyType.DEFAULT);
if (projectProp == null) {
throw new ExportException(String.format("%1$s is missing for project %2$s",
PropertyType.DEFAULT.getFilename(), relativePath));
}
ApkSettings apkSettings = new ApkSettings(projectProp);
return new ProjectConfig(projectFolder,
relativePath,
manifestData.getMinSdkVersion(),
manifestData.getGlEsVersion(),
manifestData.getSupportsScreensValues(),
apkSettings.isSplitByAbi(),
apkSettings.isSplitByDensity(),
apkSettings.getLocaleFilters());
}
private ProjectConfig(File projectFolder, String relativePath,
int minSdkVersion, int glEsVersion,
SupportsScreens supportsScreens, boolean splitByAbi, boolean splitByDensity,
Map<String, String> localeFilters) {
mProjectFolder = projectFolder;
mRelativePath = relativePath;
mMinSdkVersion = minSdkVersion;
mGlEsVersion = glEsVersion;
mSupportsScreens = supportsScreens;
mSplitByAbi = splitByAbi;
mSplitByDensity = splitByDensity;
mLocaleFilters = localeFilters;
if (mSplitByAbi) {
mAbis = findAbis();
} else {
mAbis = null;
}
}
public File getProjectFolder() {
return mProjectFolder;
}
public String getRelativePath() {
return mRelativePath;
}
List<ApkData> getApkDataList() {
// there are 3 cases:
// 1. ABI split generate multiple apks with different build info, so they are different
// ApkData for all of them. Special case: split by abi but no native code => 1 ApkData.
// 2. split by density or locale filters generate soft variant only, so they all go
// in the same ApkData.
// 3. Both 1. and 2. means that more than one ApkData are created and they all get soft
// variants.
ArrayList<ApkData> list = new ArrayList<ApkData>();
Map<String, String> softVariants = computeSoftVariantMap();
if (mSplitByAbi) {
if (mAbis.size() > 0) {
for (String abi : mAbis) {
list.add(new ApkData(this, abi, softVariants));
}
} else {
// if there are no ABIs, then just generate a single ApkData with no specific ABI.
list.add(new ApkData(this, softVariants));
}
} else {
// create a single ApkData.
list.add(new ApkData(this, softVariants));
}
return list;
}
int getMinSdkVersion() {
return mMinSdkVersion;
}
SupportsScreens getSupportsScreens() {
return mSupportsScreens;
}
int getGlEsVersion() {
return mGlEsVersion;
}
boolean isSplitByDensity() {
return mSplitByDensity;
}
boolean isSplitByAbi() {
return mSplitByAbi;
}
/**
* Returns a map of pair values (apk name suffix, aapt res filter) to be used to generate
* multiple soft apk variants.
*/
private Map<String, String> computeSoftVariantMap() {
HashMap<String, String> map = new HashMap<String, String>();
if (mSplitByDensity && mLocaleFilters.size() > 0) {
for (String[] density : DENSITY_LIST) {
for (Entry<String,String> entry : mLocaleFilters.entrySet()) {
map.put(density[0] + "-" + entry.getKey(),
density[1] + "," + entry.getValue());
}
}
} else if (mSplitByDensity) {
for (String[] density : DENSITY_LIST) {
map.put(density[0], density[1]);
}
} else if (mLocaleFilters.size() > 0) {
map.putAll(mLocaleFilters);
}
return map;
}
/**
* Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder.
*
* @param projectPath The OS path of the project.
* @return A new non-null, possibly empty, list of ABI strings.
*/
private List<String> findAbis() {
ArrayList<String> abiList = new ArrayList<String>();
File libs = new File(mProjectFolder, SdkConstants.FD_NATIVE_LIBS);
if (libs.isDirectory()) {
File[] abis = libs.listFiles();
for (File abi : abis) {
if (abi.isDirectory()) {
// only add the abi folder if there are .so files in it.
String[] content = abi.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".so");
}
});
if (content.length > 0) {
abiList.add(abi.getName());
}
}
}
}
return abiList;
}
String getConfigString(boolean onlyManifestData) {
StringBuilder sb = new StringBuilder();
LogHelper.write(sb, PROP_API, mMinSdkVersion);
LogHelper.write(sb, PROP_SCREENS, mSupportsScreens.getEncodedValues());
if (mGlEsVersion != ManifestData.GL_ES_VERSION_NOT_SET) {
LogHelper.write(sb, PROP_GL, "0x" + Integer.toHexString(mGlEsVersion));
}
if (onlyManifestData == false) {
if (mSplitByAbi) {
// need to not only encode true, but also the list of ABIs that will be used when
// the project is exported. This is because the hard property is not so much
// whether an apk is generated per ABI, but *how many* of them (since they all take
// a different build Info).
StringBuilder value = new StringBuilder(Boolean.toString(true));
for (String abi : mAbis) {
value.append('|').append(abi);
}
LogHelper.write(sb, PROP_ABI, value);
} else {
LogHelper.write(sb, PROP_ABI, false);
}
// in this case we're simply always going to make 3 versions (which may not make sense)
// so the boolean is enough.
LogHelper.write(sb, PROP_DENSITY, Boolean.toString(mSplitByDensity));
if (mLocaleFilters.size() > 0) {
LogHelper.write(sb, PROP_LOCALEFILTERS, ApkSettings.writeLocaleFilters(mLocaleFilters));
}
}
return sb.toString();
}
/**
* Compares the current project config to a list of properties.
* These properties are in the format output by {@link #getConfigString()}.
* @param values the properties to compare to.
* @return null if the properties exactly match the current config, an error message otherwise
*/
String compareToProperties(Map<String, String> values) {
String tmp;
// Note that most properties must always be present in the map.
try {
// api must always be there
if (mMinSdkVersion != Integer.parseInt(values.get(PROP_API))) {
return "Attribute minSdkVersion changed";
}
} catch (NumberFormatException e) {
// failed to convert an integer? consider the configs not equal.
return "Failed to convert attribute minSdkVersion to an Integer";
}
try {
tmp = values.get(PROP_GL); // GL is optional in the config string.
if (tmp != null) {
if (mGlEsVersion != Integer.decode(tmp)) {
return "Attribute glEsVersion changed";
}
}
} catch (NumberFormatException e) {
// failed to convert an integer? consider the configs not equal.
return "Failed to convert attribute glEsVersion to an Integer";
}
tmp = values.get(PROP_DENSITY);
if (tmp == null || mSplitByDensity != Boolean.valueOf(tmp)) {
return "Property split.density changed or is missing from config file";
}
// compare the ABI. If splitByAbi is true, then compares the ABIs present in the project
// as they must match.
tmp = values.get(PROP_ABI);
if (tmp == null) {
return "Property split.abi is missing from config file";
}
String[] abis = tmp.split("\\|");
if (mSplitByAbi != Boolean.valueOf(abis[0])) { // first value is always the split boolean
return "Property split.abi changed";
}
// now compare the rest if needed.
if (mSplitByAbi) {
if (abis.length - 1 != mAbis.size()) {
return "The number of ABIs available in the project changed";
}
for (int i = 1 ; i < abis.length ; i++) {
if (mAbis.indexOf(abis[i]) == -1) {
return "The list of ABIs available in the project changed";
}
}
}
tmp = values.get(PROP_SCREENS);
if (tmp != null) {
SupportsScreens supportsScreens = new SupportsScreens(tmp);
if (supportsScreens.equals(mSupportsScreens) == false) {
return "Supports-Screens value changed";
}
} else {
return "Supports-screens value missing from config file";
}
tmp = values.get(PROP_LOCALEFILTERS);
if (tmp != null) {
if (mLocaleFilters.equals(ApkSettings.readLocaleFilters(tmp)) == false) {
return "Locale resource filter changed";
}
} else {
// do nothing. locale filter is optional in the config string.
}
return null;
}
}