1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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.sdklib.internal.export; 18 19 import com.android.sdklib.SdkConstants; 20 import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException; 21 import com.android.sdklib.internal.project.ApkSettings; 22 import com.android.sdklib.internal.project.ProjectProperties; 23 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 24 import com.android.sdklib.resources.Density; 25 import com.android.sdklib.xml.ManifestData; 26 import com.android.sdklib.xml.ManifestData.SupportsScreens; 27 28 import java.io.File; 29 import java.io.FilenameFilter; 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Map.Entry; 35 36 /** 37 * Class representing an Android project and its properties. 38 * 39 * Only the properties that pertain to the multi-apk export are present. 40 */ 41 public final class ProjectConfig { 42 43 private static final String PROP_API = "api"; 44 private static final String PROP_SCREENS = "screens"; 45 private static final String PROP_GL = "gl"; 46 private static final String PROP_ABI = "splitByAbi"; 47 private static final String PROP_DENSITY = "splitByDensity"; 48 private static final String PROP_LOCALEFILTERS = "localeFilters"; 49 50 /** 51 * List of densities and their associated aapt filter. 52 */ 53 private static final String[][] DENSITY_LIST = new String[][] { 54 new String[] { Density.HIGH.getResourceValue(), 55 Density.HIGH.getResourceValue() + "," + Density.NODPI.getResourceValue() }, 56 new String[] { Density.MEDIUM.getResourceValue(), 57 Density.MEDIUM.getResourceValue() + "," + 58 Density.NODPI.getResourceValue() }, 59 new String[] { Density.MEDIUM.getResourceValue(), 60 Density.MEDIUM.getResourceValue() + "," + Density.NODPI.getResourceValue() }, 61 }; 62 63 private final File mProjectFolder; 64 private final String mRelativePath; 65 66 private final int mMinSdkVersion; 67 private final int mGlEsVersion; 68 private final SupportsScreens mSupportsScreens; 69 private final boolean mSplitByAbi; 70 private final boolean mSplitByDensity; 71 private final Map<String, String> mLocaleFilters; 72 /** List of ABIs not defined in the properties but actually existing in the project as valid 73 * .so files */ 74 private final List<String> mAbis; 75 76 static ProjectConfig create(File projectFolder, String relativePath, 77 ManifestData manifestData) throws ExportException { 78 // load the project properties 79 ProjectProperties projectProp = ProjectProperties.load(projectFolder.getAbsolutePath(), 80 PropertyType.DEFAULT); 81 if (projectProp == null) { 82 throw new ExportException(String.format("%1$s is missing for project %2$s", 83 PropertyType.DEFAULT.getFilename(), relativePath)); 84 } 85 86 ApkSettings apkSettings = new ApkSettings(projectProp); 87 88 return new ProjectConfig(projectFolder, 89 relativePath, 90 manifestData.getMinSdkVersion(), 91 manifestData.getGlEsVersion(), 92 manifestData.getSupportsScreensValues(), 93 apkSettings.isSplitByAbi(), 94 apkSettings.isSplitByDensity(), 95 apkSettings.getLocaleFilters()); 96 } 97 98 99 private ProjectConfig(File projectFolder, String relativePath, 100 int minSdkVersion, int glEsVersion, 101 SupportsScreens supportsScreens, boolean splitByAbi, boolean splitByDensity, 102 Map<String, String> localeFilters) { 103 mProjectFolder = projectFolder; 104 mRelativePath = relativePath; 105 mMinSdkVersion = minSdkVersion; 106 mGlEsVersion = glEsVersion; 107 mSupportsScreens = supportsScreens; 108 mSplitByAbi = splitByAbi; 109 mSplitByDensity = splitByDensity; 110 mLocaleFilters = localeFilters; 111 if (mSplitByAbi) { 112 mAbis = findAbis(); 113 } else { 114 mAbis = null; 115 } 116 } 117 118 public File getProjectFolder() { 119 return mProjectFolder; 120 } 121 122 123 public String getRelativePath() { 124 return mRelativePath; 125 } 126 127 List<ApkData> getApkDataList() { 128 // there are 3 cases: 129 // 1. ABI split generate multiple apks with different build info, so they are different 130 // ApkData for all of them. Special case: split by abi but no native code => 1 ApkData. 131 // 2. split by density or locale filters generate soft variant only, so they all go 132 // in the same ApkData. 133 // 3. Both 1. and 2. means that more than one ApkData are created and they all get soft 134 // variants. 135 136 ArrayList<ApkData> list = new ArrayList<ApkData>(); 137 138 Map<String, String> softVariants = computeSoftVariantMap(); 139 140 if (mSplitByAbi) { 141 if (mAbis.size() > 0) { 142 for (String abi : mAbis) { 143 list.add(new ApkData(this, abi, softVariants)); 144 } 145 } else { 146 // if there are no ABIs, then just generate a single ApkData with no specific ABI. 147 list.add(new ApkData(this, softVariants)); 148 } 149 } else { 150 // create a single ApkData. 151 list.add(new ApkData(this, softVariants)); 152 } 153 154 return list; 155 } 156 157 int getMinSdkVersion() { 158 return mMinSdkVersion; 159 } 160 161 SupportsScreens getSupportsScreens() { 162 return mSupportsScreens; 163 } 164 165 int getGlEsVersion() { 166 return mGlEsVersion; 167 } 168 169 boolean isSplitByDensity() { 170 return mSplitByDensity; 171 } 172 173 boolean isSplitByAbi() { 174 return mSplitByAbi; 175 } 176 177 /** 178 * Returns a map of pair values (apk name suffix, aapt res filter) to be used to generate 179 * multiple soft apk variants. 180 */ 181 private Map<String, String> computeSoftVariantMap() { 182 HashMap<String, String> map = new HashMap<String, String>(); 183 184 if (mSplitByDensity && mLocaleFilters.size() > 0) { 185 for (String[] density : DENSITY_LIST) { 186 for (Entry<String,String> entry : mLocaleFilters.entrySet()) { 187 map.put(density[0] + "-" + entry.getKey(), 188 density[1] + "," + entry.getValue()); 189 } 190 } 191 192 } else if (mSplitByDensity) { 193 for (String[] density : DENSITY_LIST) { 194 map.put(density[0], density[1]); 195 } 196 197 } else if (mLocaleFilters.size() > 0) { 198 map.putAll(mLocaleFilters); 199 200 } 201 202 return map; 203 } 204 205 /** 206 * Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder. 207 * 208 * @param projectPath The OS path of the project. 209 * @return A new non-null, possibly empty, list of ABI strings. 210 */ 211 private List<String> findAbis() { 212 ArrayList<String> abiList = new ArrayList<String>(); 213 File libs = new File(mProjectFolder, SdkConstants.FD_NATIVE_LIBS); 214 if (libs.isDirectory()) { 215 File[] abis = libs.listFiles(); 216 for (File abi : abis) { 217 if (abi.isDirectory()) { 218 // only add the abi folder if there are .so files in it. 219 String[] content = abi.list(new FilenameFilter() { 220 public boolean accept(File dir, String name) { 221 return name.toLowerCase().endsWith(".so"); 222 } 223 }); 224 225 if (content.length > 0) { 226 abiList.add(abi.getName()); 227 } 228 } 229 } 230 } 231 232 return abiList; 233 } 234 235 String getConfigString(boolean onlyManifestData) { 236 StringBuilder sb = new StringBuilder(); 237 LogHelper.write(sb, PROP_API, mMinSdkVersion); 238 LogHelper.write(sb, PROP_SCREENS, mSupportsScreens.getEncodedValues()); 239 240 if (mGlEsVersion != ManifestData.GL_ES_VERSION_NOT_SET) { 241 LogHelper.write(sb, PROP_GL, "0x" + Integer.toHexString(mGlEsVersion)); 242 } 243 244 if (onlyManifestData == false) { 245 if (mSplitByAbi) { 246 // need to not only encode true, but also the list of ABIs that will be used when 247 // the project is exported. This is because the hard property is not so much 248 // whether an apk is generated per ABI, but *how many* of them (since they all take 249 // a different build Info). 250 StringBuilder value = new StringBuilder(Boolean.toString(true)); 251 for (String abi : mAbis) { 252 value.append('|').append(abi); 253 } 254 LogHelper.write(sb, PROP_ABI, value); 255 } else { 256 LogHelper.write(sb, PROP_ABI, false); 257 } 258 259 // in this case we're simply always going to make 3 versions (which may not make sense) 260 // so the boolean is enough. 261 LogHelper.write(sb, PROP_DENSITY, Boolean.toString(mSplitByDensity)); 262 263 if (mLocaleFilters.size() > 0) { 264 LogHelper.write(sb, PROP_LOCALEFILTERS, ApkSettings.writeLocaleFilters(mLocaleFilters)); 265 } 266 } 267 268 return sb.toString(); 269 } 270 271 /** 272 * Compares the current project config to a list of properties. 273 * These properties are in the format output by {@link #getConfigString()}. 274 * @param values the properties to compare to. 275 * @return null if the properties exactly match the current config, an error message otherwise 276 */ 277 String compareToProperties(Map<String, String> values) { 278 String tmp; 279 // Note that most properties must always be present in the map. 280 try { 281 // api must always be there 282 if (mMinSdkVersion != Integer.parseInt(values.get(PROP_API))) { 283 return "Attribute minSdkVersion changed"; 284 } 285 } catch (NumberFormatException e) { 286 // failed to convert an integer? consider the configs not equal. 287 return "Failed to convert attribute minSdkVersion to an Integer"; 288 } 289 290 try { 291 tmp = values.get(PROP_GL); // GL is optional in the config string. 292 if (tmp != null) { 293 if (mGlEsVersion != Integer.decode(tmp)) { 294 return "Attribute glEsVersion changed"; 295 } 296 } 297 } catch (NumberFormatException e) { 298 // failed to convert an integer? consider the configs not equal. 299 return "Failed to convert attribute glEsVersion to an Integer"; 300 } 301 302 tmp = values.get(PROP_DENSITY); 303 if (tmp == null || mSplitByDensity != Boolean.valueOf(tmp)) { 304 return "Property split.density changed or is missing from config file"; 305 } 306 307 // compare the ABI. If splitByAbi is true, then compares the ABIs present in the project 308 // as they must match. 309 tmp = values.get(PROP_ABI); 310 if (tmp == null) { 311 return "Property split.abi is missing from config file"; 312 } 313 String[] abis = tmp.split("\\|"); 314 if (mSplitByAbi != Boolean.valueOf(abis[0])) { // first value is always the split boolean 315 return "Property split.abi changed"; 316 } 317 // now compare the rest if needed. 318 if (mSplitByAbi) { 319 if (abis.length - 1 != mAbis.size()) { 320 return "The number of ABIs available in the project changed"; 321 } 322 for (int i = 1 ; i < abis.length ; i++) { 323 if (mAbis.indexOf(abis[i]) == -1) { 324 return "The list of ABIs available in the project changed"; 325 } 326 } 327 } 328 329 tmp = values.get(PROP_SCREENS); 330 if (tmp != null) { 331 SupportsScreens supportsScreens = new SupportsScreens(tmp); 332 if (supportsScreens.equals(mSupportsScreens) == false) { 333 return "Supports-Screens value changed"; 334 } 335 } else { 336 return "Supports-screens value missing from config file"; 337 } 338 339 tmp = values.get(PROP_LOCALEFILTERS); 340 if (tmp != null) { 341 if (mLocaleFilters.equals(ApkSettings.readLocaleFilters(tmp)) == false) { 342 return "Locale resource filter changed"; 343 } 344 } else { 345 // do nothing. locale filter is optional in the config string. 346 } 347 348 return null; 349 } 350 } 351