├── .classpath ├── src └── ca │ └── skennedy │ └── androidunusedresources │ ├── Loader.java │ ├── FileType.java │ ├── ResourceType.java │ ├── FileUtilities.java │ ├── Resource.java │ ├── UsageMatrix.java │ └── ResourceScanner.java └── .project /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/Loader.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | public class Loader { 4 | private Loader() { 5 | super(); 6 | } 7 | 8 | public static void main(final String[] args) { 9 | final ResourceScanner resourceScanner = new ResourceScanner(); 10 | resourceScanner.run(args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Android Unused Resources 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/FileType.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class FileType { 6 | private final String mExtension; 7 | private final String mUsage; 8 | 9 | public static final String USAGE_TYPE = "{type}"; 10 | public static final String USAGE_NAME = "{name}"; 11 | 12 | public FileType(final String extension, final String usage) { 13 | super(); 14 | mExtension = extension; 15 | mUsage = usage; 16 | } 17 | 18 | public String getExtension() { 19 | return mExtension; 20 | } 21 | 22 | public Pattern getPattern(final String type, final String name) { 23 | return Pattern.compile(mUsage.replace(USAGE_TYPE, type).replace(USAGE_NAME, name)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/ResourceType.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.io.File; 4 | 5 | public abstract class ResourceType { 6 | private final String mType; 7 | 8 | public ResourceType(final String type) { 9 | super(); 10 | mType = type; 11 | } 12 | 13 | public String getType() { 14 | return mType; 15 | } 16 | 17 | public abstract boolean doesFileDeclareResource(File parent, String fileName, String fileContents, String resourceName); 18 | 19 | /** 20 | * Scans a file for special uses of the resource (i.e. not a simple string match on the resource name). 21 | * 22 | * @param parent 23 | * @param fileName 24 | * @param fileContents 25 | * @param resourceName 26 | * @return true if used, false otherwise 27 | */ 28 | public boolean doesFileUseResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/FileUtilities.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class FileUtilities { 13 | private FileUtilities() { 14 | super(); 15 | } 16 | 17 | public static String getFileContents(final File file) throws IOException { 18 | final InputStream inputStream = new FileInputStream(file); 19 | final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 20 | 21 | final StringBuilder stringBuilder = new StringBuilder(); 22 | 23 | boolean done = false; 24 | 25 | while (!done) { 26 | final String line = reader.readLine(); 27 | done = (line == null); 28 | 29 | if (line != null) { 30 | stringBuilder.append(line); 31 | } 32 | } 33 | 34 | reader.close(); 35 | inputStream.close(); 36 | 37 | return stringBuilder.toString(); 38 | } 39 | 40 | public static List getFileLines(final File file) throws IOException { 41 | final InputStream inputStream = new FileInputStream(file); 42 | final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 43 | 44 | final List lines = new ArrayList(); 45 | 46 | boolean done = false; 47 | 48 | while (!done) { 49 | final String line = reader.readLine(); 50 | done = (line == null); 51 | 52 | if (line != null) { 53 | lines.add(line); 54 | } 55 | } 56 | 57 | reader.close(); 58 | inputStream.close(); 59 | 60 | return lines; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/Resource.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import java.util.SortedSet; 6 | import java.util.TreeSet; 7 | 8 | public class Resource implements Comparable { 9 | private final String mType; 10 | private final String mName; 11 | 12 | private final SortedSet mDeclaredPaths = new TreeSet(); 13 | private final Set mConfigurations = new HashSet(); 14 | 15 | private static final String sStringFormat = "%-10s: %s"; 16 | private static final String sPathFormat = " %s"; 17 | 18 | public Resource(final String type, final String name) { 19 | super(); 20 | mType = type; 21 | mName = name; 22 | } 23 | 24 | public String getType() { 25 | return mType; 26 | } 27 | 28 | public String getName() { 29 | return mName; 30 | } 31 | 32 | public void addDeclaredPath(final String path) { 33 | mDeclaredPaths.add(path); 34 | } 35 | 36 | public boolean hasNoDeclaredPaths() { 37 | return mDeclaredPaths.isEmpty(); 38 | } 39 | 40 | public void addConfiguration(final String configuration) { 41 | mConfigurations.add(configuration); 42 | } 43 | 44 | public Set getConfigurations() { 45 | return mConfigurations; 46 | } 47 | 48 | @Override 49 | public int compareTo(final Resource another) { 50 | final int typeComparison = mType.compareTo(another.getType()); 51 | 52 | if (typeComparison != 0) { 53 | return typeComparison; 54 | } 55 | 56 | return mName.compareTo(another.getName()); 57 | } 58 | 59 | @Override 60 | public boolean equals(final Object o) { 61 | if (o == null || !(o instanceof Resource)) { 62 | return false; 63 | } 64 | 65 | final Resource resource = (Resource) o; 66 | 67 | return mType.equals(resource.getType()) && mName.equals(resource.getName()); 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | return (mType + '/' + mName).hashCode(); 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | final StringBuilder stringBuilder = new StringBuilder(String.format(sStringFormat, mType, mName)); 78 | 79 | for (final String path : mDeclaredPaths) { 80 | stringBuilder.append('\n'); 81 | stringBuilder.append(String.format(sPathFormat, path)); 82 | } 83 | 84 | return stringBuilder.toString(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/UsageMatrix.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.io.Writer; 8 | import java.util.HashMap; 9 | import java.util.LinkedHashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.SortedMap; 13 | 14 | /** 15 | * Generates a usage matrix that lists the various configurations each resource is defined under. 16 | */ 17 | public class UsageMatrix { 18 | private final File mBaseDirectory; 19 | 20 | /** 21 | *

22 | * ResourceType->(ResourceName->Resource) 23 | *

24 | *

25 | * string->(app_name->Resource) 26 | *

27 | */ 28 | private final SortedMap> mResources; 29 | 30 | /** 31 | *

32 | * ResourceType->(Configuration) 33 | *

34 | *

35 | * string->(en-rUS) 36 | *

37 | */ 38 | private final Map> mConfigurations = new HashMap>(); 39 | 40 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 41 | 42 | public UsageMatrix(final File baseDirectory, final SortedMap> resources) { 43 | mBaseDirectory = baseDirectory; 44 | mResources = resources; 45 | } 46 | 47 | public void generateMatrices() { 48 | final File matrixDirectory = new File(mBaseDirectory, "resource-matrices"); 49 | 50 | if (!matrixDirectory.exists()) { 51 | System.out 52 | .println("Not generating resource qualifier matrices. If you would like them, create a directory named 'resource-matrices' in the base of your project."); 53 | System.out.println(); 54 | return; 55 | } 56 | 57 | System.out.println("Resource qualifier matrices generated."); 58 | System.out.println(); 59 | 60 | generateConfigurationList(); 61 | 62 | for (final String resourceType : mResources.keySet()) { 63 | final File resourceMatrix = new File(matrixDirectory, resourceType + ".csv"); 64 | 65 | try { 66 | final Writer writer = new BufferedWriter(new FileWriter(resourceMatrix)); 67 | writer.write(buildCsv(resourceType)); 68 | writer.close(); 69 | } catch (final IOException e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | } 74 | 75 | private void generateConfigurationList() { 76 | mConfigurations.clear(); 77 | 78 | // Resource types 79 | for (final String resourceType : mResources.keySet()) { 80 | // Set up the resource type 81 | final LinkedHashSet configurations = new LinkedHashSet(); 82 | mConfigurations.put(resourceType, configurations); 83 | 84 | final SortedMap resources = mResources.get(resourceType); 85 | 86 | // All the resources of this type 87 | for (final Resource resource : resources.values()) { 88 | // Paths where it exists 89 | for (final String configuration : resource.getConfigurations()) { 90 | configurations.add(configuration); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Generates the CSV for a given resource type 98 | * 99 | * @param resourceType 100 | * The resource type for which to build the CSV 101 | * @return a {@link String} in the format: 102 | * 103 | *
104 |      * ,ldpi,mdpi,hdpi,xhdpi
105 |      * resource-name0,,X,X,
106 |      * resource-name2,,X,X,X
107 |      * resource-name1,,X,,
108 |      * 
109 | */ 110 | private String buildCsv(final String resourceType) { 111 | final StringBuilder stringBuilder = new StringBuilder(); 112 | 113 | final Set configurations = mConfigurations.get(resourceType); 114 | 115 | // Header row 116 | for (final String configuration : configurations) { 117 | stringBuilder.append(',').append(configuration); 118 | } 119 | 120 | // Resource rows 121 | for (final Resource resource : mResources.get(resourceType).values()) { 122 | stringBuilder.append(LINE_SEPARATOR).append(resource.getName()); 123 | 124 | final Set resourceConfigurations = resource.getConfigurations(); 125 | 126 | for (final String configuration : configurations) { 127 | stringBuilder.append(','); 128 | 129 | if (resourceConfigurations.contains(configuration)) { 130 | stringBuilder.append('X'); 131 | } 132 | } 133 | } 134 | 135 | return stringBuilder.toString(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/ca/skennedy/androidunusedresources/ResourceScanner.java: -------------------------------------------------------------------------------- 1 | package ca.skennedy.androidunusedresources; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.SortedMap; 16 | import java.util.SortedSet; 17 | import java.util.TreeMap; 18 | import java.util.TreeSet; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | public class ResourceScanner { 23 | private final File mBaseDirectory; 24 | 25 | private File mSrcDirectory = null; 26 | private File mResDirectory = null; 27 | private File mGenDirectory = null; 28 | 29 | private File mManifestFile = null; 30 | private File mRJavaFile = null; 31 | private String mPackageName = null; 32 | 33 | private final Set mResources = new HashSet(); 34 | private final Set mUsedResources = new HashSet(); 35 | 36 | private static final Pattern sResourceTypePattern = Pattern.compile("^\\s*public static final class (\\w+)\\s*\\{$"); 37 | private static final Pattern sResourceNamePattern = Pattern 38 | .compile("^\\s*public static( final)? int(\\[\\])? (\\w+)\\s*=\\s*(\\{|(0x)?[0-9A-Fa-f]+;)\\s*$"); 39 | 40 | private static final FileType sJavaFileType = new FileType("java", "R." + FileType.USAGE_TYPE + "." + FileType.USAGE_NAME + "[^\\w_]"); 41 | private static final FileType sXmlFileType = new FileType("xml", "[\" >]@" + FileType.USAGE_TYPE + "/" + FileType.USAGE_NAME + "[\" <]"); 42 | 43 | private static final Map sResourceTypes = new HashMap(); 44 | 45 | static { 46 | // anim 47 | sResourceTypes.put("anim", new ResourceType("anim") { 48 | @Override 49 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 50 | // Check if we're in a valid directory 51 | if (!parent.isDirectory()) { 52 | return false; 53 | } 54 | 55 | final String directoryType = parent.getName().split("-")[0]; 56 | if (!directoryType.equals(getType())) { 57 | return false; 58 | } 59 | 60 | // Check if the resource is declared here 61 | final String name = fileName.split("\\.")[0]; 62 | 63 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 64 | 65 | return pattern.matcher(name).find(); 66 | } 67 | }); 68 | 69 | // array 70 | sResourceTypes.put("array", new ResourceType("array") { 71 | @Override 72 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 73 | // Check if we're in a valid directory 74 | if (!parent.isDirectory()) { 75 | return false; 76 | } 77 | 78 | final String directoryType = parent.getName().split("-")[0]; 79 | if (!directoryType.equals("values")) { 80 | return false; 81 | } 82 | 83 | // Check if the resource is declared here 84 | final Pattern pattern = Pattern.compile("<([a-z]+\\-)?array.*?name\\s*=\\s*\"" + resourceName + "\".*?/?>"); 85 | 86 | final Matcher matcher = pattern.matcher(fileContents); 87 | 88 | if (matcher.find()) { 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | }); 95 | 96 | // attr 97 | sResourceTypes.put("attr", new ResourceType("attr") { 98 | @Override 99 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 100 | // Check if we're in a valid directory 101 | if (!parent.isDirectory()) { 102 | return false; 103 | } 104 | 105 | final String directoryType = parent.getName().split("-")[0]; 106 | if (!directoryType.equals("values")) { 107 | return false; 108 | } 109 | 110 | // Check if the resource is declared here 111 | final Pattern pattern = Pattern.compile(""); 112 | 113 | final Matcher matcher = pattern.matcher(fileContents); 114 | 115 | if (matcher.find()) { 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | 122 | @Override 123 | public boolean doesFileUseResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 124 | if (parent != null) { 125 | // Check if we're in a valid directory 126 | if (!parent.isDirectory()) { 127 | return false; 128 | } 129 | 130 | final String directoryType = parent.getName().split("-")[0]; 131 | if (!directoryType.equals("layout") && !directoryType.equals("values")) { 132 | return false; 133 | } 134 | } 135 | 136 | // Check if the attribute is used here 137 | // TODO: This can fail to report attrs as unused even when they're never used. Make it better, but don't allow any false positives. 138 | final Pattern pattern = Pattern.compile("<.+?:" + resourceName + "\\s*=\\s*\".*?\".*?/?>"); 139 | 140 | final Matcher matcher = pattern.matcher(fileContents); 141 | 142 | if (matcher.find()) { 143 | return true; 144 | } 145 | 146 | final Pattern itemPattern = Pattern.compile(""); 147 | final Matcher itemMatcher = itemPattern.matcher(fileContents); 148 | 149 | if (itemMatcher.find()) { 150 | return true; 151 | } 152 | 153 | return false; 154 | } 155 | }); 156 | 157 | // bool 158 | sResourceTypes.put("bool", new ResourceType("bool") { 159 | @Override 160 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 161 | // Check if we're in a valid directory 162 | if (!parent.isDirectory()) { 163 | return false; 164 | } 165 | 166 | final String directoryType = parent.getName().split("-")[0]; 167 | if (!directoryType.equals("values")) { 168 | return false; 169 | } 170 | 171 | // Check if the resource is declared here 172 | final Pattern pattern = Pattern.compile(""); 173 | 174 | final Matcher matcher = pattern.matcher(fileContents); 175 | 176 | if (matcher.find()) { 177 | return true; 178 | } 179 | 180 | return false; 181 | } 182 | }); 183 | 184 | // color 185 | sResourceTypes.put("color", new ResourceType("color") { 186 | @Override 187 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 188 | // Check if we're in a valid directory 189 | if (!parent.isDirectory()) { 190 | return false; 191 | } 192 | 193 | final String directoryType = parent.getName().split("-")[0]; 194 | if (!directoryType.equals("values")) { 195 | return false; 196 | } 197 | 198 | // Check if the resource is declared here 199 | final Pattern pattern = Pattern.compile(""); 200 | 201 | final Matcher matcher = pattern.matcher(fileContents); 202 | 203 | if (matcher.find()) { 204 | return true; 205 | } 206 | 207 | return false; 208 | } 209 | }); 210 | 211 | // dimen 212 | sResourceTypes.put("dimen", new ResourceType("dimen") { 213 | @Override 214 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 215 | // Check if we're in a valid directory 216 | if (!parent.isDirectory()) { 217 | return false; 218 | } 219 | 220 | final String directoryType = parent.getName().split("-")[0]; 221 | if (!directoryType.equals("values")) { 222 | return false; 223 | } 224 | 225 | // Check if the resource is declared here 226 | final Pattern pattern = Pattern.compile(""); 227 | 228 | final Matcher matcher = pattern.matcher(fileContents); 229 | 230 | if (matcher.find()) { 231 | return true; 232 | } 233 | 234 | return false; 235 | } 236 | }); 237 | 238 | // drawable 239 | sResourceTypes.put("drawable", new ResourceType("drawable") { 240 | @Override 241 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 242 | // Check if we're in a valid directory 243 | if (!parent.isDirectory()) { 244 | return false; 245 | } 246 | 247 | final String directoryType = parent.getName().split("-")[0]; 248 | if (directoryType.equals(getType())) { 249 | // We're in a drawable- directory 250 | 251 | // Check if the resource is declared here 252 | final String name = fileName.split("\\.")[0]; 253 | 254 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 255 | 256 | return pattern.matcher(name).find(); 257 | } 258 | 259 | if (directoryType.equals("values")) { 260 | // We're in a values- directory 261 | 262 | // Check if the resource is declared here 263 | final Pattern pattern = Pattern.compile(""); 264 | 265 | final Matcher matcher = pattern.matcher(fileContents); 266 | 267 | if (matcher.find()) { 268 | return true; 269 | } 270 | } 271 | 272 | return false; 273 | } 274 | }); 275 | 276 | // id 277 | sResourceTypes.put("id", new ResourceType("id") { 278 | @Override 279 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 280 | // Check if we're in a valid directory 281 | if (!parent.isDirectory()) { 282 | return false; 283 | } 284 | 285 | final String directoryType = parent.getName().split("-")[0]; 286 | if (!directoryType.equals("values") && !directoryType.equals("layout")) { 287 | return false; 288 | } 289 | 290 | // Check if the resource is declared here 291 | final Pattern valuesPattern0 = Pattern.compile(""); 292 | final Pattern valuesPattern1 = Pattern.compile(""); 293 | final Pattern layoutPattern = Pattern.compile(":id\\s*=\\s*\"@\\+id/" + resourceName + "\""); 294 | 295 | Matcher matcher = valuesPattern0.matcher(fileContents); 296 | 297 | if (matcher.find()) { 298 | return true; 299 | } 300 | 301 | matcher = valuesPattern1.matcher(fileContents); 302 | 303 | if (matcher.find()) { 304 | return true; 305 | } 306 | 307 | matcher = layoutPattern.matcher(fileContents); 308 | 309 | if (matcher.find()) { 310 | return true; 311 | } 312 | 313 | return false; 314 | } 315 | }); 316 | 317 | // integer 318 | sResourceTypes.put("integer", new ResourceType("integer") { 319 | @Override 320 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 321 | // Check if we're in a valid directory 322 | if (!parent.isDirectory()) { 323 | return false; 324 | } 325 | 326 | final String directoryType = parent.getName().split("-")[0]; 327 | if (!directoryType.equals("values")) { 328 | return false; 329 | } 330 | 331 | // Check if the resource is declared here 332 | final Pattern pattern = Pattern.compile(""); 333 | 334 | final Matcher matcher = pattern.matcher(fileContents); 335 | 336 | if (matcher.find()) { 337 | return true; 338 | } 339 | 340 | return false; 341 | } 342 | }); 343 | 344 | // layout 345 | sResourceTypes.put("layout", new ResourceType("layout") { 346 | @Override 347 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 348 | // Check if we're in a valid directory 349 | if (!parent.isDirectory()) { 350 | return false; 351 | } 352 | 353 | final String directoryType = parent.getName().split("-")[0]; 354 | if (!directoryType.equals(getType())) { 355 | return false; 356 | } 357 | 358 | // Check if the resource is declared here 359 | final String name = fileName.split("\\.")[0]; 360 | 361 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 362 | 363 | return pattern.matcher(name).find(); 364 | } 365 | }); 366 | 367 | // menu 368 | sResourceTypes.put("menu", new ResourceType("menu") { 369 | @Override 370 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 371 | // Check if we're in a valid directory 372 | if (!parent.isDirectory()) { 373 | return false; 374 | } 375 | 376 | final String directoryType = parent.getName().split("-")[0]; 377 | if (!directoryType.equals(getType())) { 378 | return false; 379 | } 380 | 381 | // Check if the resource is declared here 382 | final String name = fileName.split("\\.")[0]; 383 | 384 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 385 | 386 | return pattern.matcher(name).find(); 387 | } 388 | }); 389 | 390 | // plurals 391 | sResourceTypes.put("plurals", new ResourceType("plurals") { 392 | @Override 393 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 394 | // Check if we're in a valid directory 395 | if (!parent.isDirectory()) { 396 | return false; 397 | } 398 | 399 | final String directoryType = parent.getName().split("-")[0]; 400 | if (!directoryType.equals("values")) { 401 | return false; 402 | } 403 | 404 | // Check if the resource is declared here 405 | final Pattern pattern = Pattern.compile(""); 406 | 407 | final Matcher matcher = pattern.matcher(fileContents); 408 | 409 | if (matcher.find()) { 410 | return true; 411 | } 412 | 413 | return false; 414 | } 415 | }); 416 | 417 | // raw 418 | sResourceTypes.put("raw", new ResourceType("raw") { 419 | @Override 420 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 421 | // Check if we're in a valid directory 422 | if (!parent.isDirectory()) { 423 | return false; 424 | } 425 | 426 | final String directoryType = parent.getName().split("-")[0]; 427 | if (!directoryType.equals(getType())) { 428 | return false; 429 | } 430 | 431 | // Check if the resource is declared here 432 | final String name = fileName.split("\\.")[0]; 433 | 434 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 435 | 436 | return pattern.matcher(name).find(); 437 | } 438 | }); 439 | 440 | // string 441 | sResourceTypes.put("string", new ResourceType("string") { 442 | @Override 443 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 444 | // Check if we're in a valid directory 445 | if (!parent.isDirectory()) { 446 | return false; 447 | } 448 | 449 | final String directoryType = parent.getName().split("-")[0]; 450 | if (!directoryType.equals("values")) { 451 | return false; 452 | } 453 | 454 | // Check if the resource is declared here 455 | final Pattern pattern = Pattern.compile(""); 456 | 457 | final Matcher matcher = pattern.matcher(fileContents); 458 | 459 | if (matcher.find()) { 460 | return true; 461 | } 462 | 463 | return false; 464 | } 465 | }); 466 | 467 | // style 468 | sResourceTypes.put("style", new ResourceType("style") { 469 | @Override 470 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 471 | // Check if we're in a valid directory 472 | if (!parent.isDirectory()) { 473 | return false; 474 | } 475 | 476 | final String directoryType = parent.getName().split("-")[0]; 477 | if (!directoryType.equals("values")) { 478 | return false; 479 | } 480 | 481 | // Check if the resource is declared here 482 | final Pattern pattern = Pattern.compile(""); 483 | 484 | final Matcher matcher = pattern.matcher(fileContents); 485 | 486 | if (matcher.find()) { 487 | return true; 488 | } 489 | 490 | return false; 491 | } 492 | 493 | @Override 494 | public boolean doesFileUseResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 495 | if (parent != null) { 496 | // Check if we're in a valid directory 497 | if (!parent.isDirectory()) { 498 | return false; 499 | } 500 | 501 | final String directoryType = parent.getName().split("-")[0]; 502 | if (!directoryType.equals("values")) { 503 | return false; 504 | } 505 | } 506 | 507 | // Check if the resource is used here as a parent (name="Parent.Child") 508 | final Pattern pattern = Pattern.compile(""); 509 | 510 | final Matcher matcher = pattern.matcher(fileContents); 511 | 512 | if (matcher.find()) { 513 | return true; 514 | } 515 | 516 | // Check if the resource is used here as a parent (parent="Parent") 517 | final Pattern pattern1 = Pattern.compile(""); 518 | 519 | final Matcher matcher1 = pattern1.matcher(fileContents); 520 | 521 | if (matcher1.find()) { 522 | return true; 523 | } 524 | 525 | return false; 526 | } 527 | }); 528 | 529 | // styleable 530 | sResourceTypes.put("styleable", new ResourceType("styleable") { 531 | @Override 532 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 533 | // Check if we're in a valid directory 534 | if (!parent.isDirectory()) { 535 | return false; 536 | } 537 | 538 | final String directoryType = parent.getName().split("-")[0]; 539 | if (!directoryType.equals("values")) { 540 | return false; 541 | } 542 | 543 | // Check if the resource is declared here 544 | final String[] styleableAttr = resourceName.split("\\[_\\\\.\\]"); 545 | 546 | if (styleableAttr.length == 1) { 547 | // This is the name of the styleable, not one of its attributes 548 | final Pattern pattern = Pattern.compile(""); 549 | final Matcher matcher = pattern.matcher(fileContents); 550 | 551 | if (matcher.find()) { 552 | return true; 553 | } 554 | 555 | return false; 556 | } 557 | 558 | // It's one of the attributes, like Styleable_attribute 559 | final Pattern blockPattern = Pattern.compile("(.*?)"); 560 | final Matcher blockMatcher = blockPattern.matcher(fileContents); 561 | 562 | if (blockMatcher.find()) { 563 | final String styleableAttributes = blockMatcher.group(1); 564 | 565 | // We now have just the attributes for the styleable 566 | final Pattern attributePattern = Pattern.compile(""); 567 | final Matcher attributeMatcher = attributePattern.matcher(styleableAttributes); 568 | 569 | if (attributeMatcher.find()) { 570 | return true; 571 | } 572 | 573 | return false; 574 | } 575 | 576 | return false; 577 | } 578 | }); 579 | 580 | // xml 581 | sResourceTypes.put("xml", new ResourceType("xml") { 582 | @Override 583 | public boolean doesFileDeclareResource(final File parent, final String fileName, final String fileContents, final String resourceName) { 584 | // Check if we're in a valid directory 585 | if (!parent.isDirectory()) { 586 | return false; 587 | } 588 | 589 | final String directoryType = parent.getName().split("-")[0]; 590 | if (!directoryType.equals(getType())) { 591 | return false; 592 | } 593 | 594 | // Check if the resource is declared here 595 | final String name = fileName.split("\\.")[0]; 596 | 597 | final Pattern pattern = Pattern.compile("^" + resourceName + "$"); 598 | 599 | return pattern.matcher(name).find(); 600 | } 601 | }); 602 | } 603 | 604 | public ResourceScanner() { 605 | super(); 606 | final String baseDirectory = System.getProperty("user.dir"); 607 | mBaseDirectory = new File(baseDirectory); 608 | } 609 | 610 | /** 611 | * This constructor is only used for debugging. 612 | * 613 | * @param baseDirectory 614 | * The project directory to use. 615 | */ 616 | protected ResourceScanner(final String baseDirectory) { 617 | super(); 618 | mBaseDirectory = new File(baseDirectory); 619 | } 620 | 621 | private File findGenDirectory() { 622 | String base = System.getenv("OUT_DIR"); 623 | String currentProject = mBaseDirectory.getName(); 624 | File genDir = new File(base, "target/common/obj/APPS/" 625 | + currentProject + "_intermediates/src"); 626 | if (genDir.exists()) { 627 | return genDir; 628 | } else { 629 | return null; 630 | } 631 | } 632 | 633 | public void run(String[] args) { 634 | System.out.println("Running in: " + mBaseDirectory.getAbsolutePath()); 635 | 636 | boolean isAosp = false; 637 | if (args.length > 0 && args[0].equals("--aosp")) { 638 | if (System.getenv("OUT_DIR") == null) { 639 | System.out.println("Please setup your build environment"); 640 | return; 641 | } 642 | isAosp = true; 643 | } 644 | 645 | findPaths(); 646 | 647 | if (isAosp) { 648 | mGenDirectory = findGenDirectory(); 649 | } 650 | 651 | if (mSrcDirectory == null || mResDirectory == null || mManifestFile == null) { 652 | System.err.println("The current directory is not a valid Android project root."); 653 | return; 654 | } 655 | 656 | mPackageName = findPackageName(mManifestFile); 657 | 658 | if (mPackageName == null || mPackageName.trim().length() == 0) { 659 | System.err.println("Unable to determine your application's package name from AndroidManifest.xml. Please ensure it is set."); 660 | return; 661 | } 662 | 663 | if (mGenDirectory == null) { 664 | System.err.println("You must first build your project to generate R.java"); 665 | return; 666 | } 667 | 668 | mRJavaFile = findRJavaFile(mGenDirectory, mPackageName); 669 | 670 | if (mRJavaFile == null) { 671 | System.err.println("You must first build your project to generate R.java"); 672 | return; 673 | } 674 | 675 | mResources.clear(); 676 | 677 | try { 678 | mResources.addAll(getResourceList(mRJavaFile)); 679 | } catch (final IOException e) { 680 | System.err.println("The R.java found could not be opened."); 681 | e.printStackTrace(); 682 | } 683 | 684 | System.out.println(mResources.size() + " resources found"); 685 | System.out.println(); 686 | 687 | mUsedResources.clear(); 688 | 689 | searchFiles(null, mSrcDirectory, sJavaFileType); 690 | searchFiles(null, mResDirectory, sXmlFileType); 691 | searchFiles(null, mManifestFile, sXmlFileType); 692 | 693 | /* 694 | * Because attr and styleable are so closely linked, we need to do some matching now to ensure we don't say an attr is unused if its corresponding 695 | * styleable is used. 696 | */ 697 | final Set extraUsedResources = new HashSet(); 698 | 699 | for (final Resource resource : mResources) { 700 | if (resource.getType().equals("styleable")) { 701 | final String[] styleableAttr = resource.getName().split("_"); 702 | 703 | if (styleableAttr.length > 1) { 704 | final String attrName = styleableAttr[1]; 705 | 706 | final Resource attrResourceTest = new Resource("attr", attrName); 707 | 708 | if (mUsedResources.contains(attrResourceTest)) { 709 | // It's used 710 | extraUsedResources.add(resource); 711 | } 712 | } 713 | } else if (resource.getType().equals("attr")) { 714 | // Check if we use this attr as a styleable 715 | for (final Resource usedResource : mUsedResources) { 716 | if (usedResource.getType().equals("styleable")) { 717 | final String[] styleableAttr = usedResource.getName().split("_"); 718 | 719 | if (styleableAttr.length > 1 && styleableAttr[1].equals(resource.getName())) { 720 | // It's used 721 | extraUsedResources.add(resource); 722 | } 723 | } 724 | } 725 | } 726 | } 727 | 728 | // Move the new found used resources to the used set 729 | for (final Resource resource : extraUsedResources) { 730 | mResources.remove(resource); 731 | mUsedResources.add(resource); 732 | } 733 | 734 | /* 735 | * Find the paths where the unused resources are declared. 736 | */ 737 | final SortedMap> unusedResources = new TreeMap>(); 738 | 739 | for (final Resource resource : mResources) { 740 | final String type = resource.getType(); 741 | SortedMap typeMap = unusedResources.get(type); 742 | 743 | if (typeMap == null) { 744 | typeMap = new TreeMap(); 745 | unusedResources.put(type, typeMap); 746 | } 747 | 748 | typeMap.put(resource.getName(), resource); 749 | } 750 | 751 | // Ensure we only try to find resource types that exist in the map we just built 752 | final Map unusedResourceTypes = new HashMap(unusedResources.size()); 753 | 754 | for (final String type : unusedResources.keySet()) { 755 | final ResourceType resourceType = sResourceTypes.get(type); 756 | if (resourceType != null) { 757 | unusedResourceTypes.put(type, resourceType); 758 | } 759 | } 760 | 761 | findDeclaredPaths(null, mResDirectory, unusedResourceTypes, unusedResources); 762 | 763 | /* 764 | * Find the paths where the used resources are declared. 765 | */ 766 | final SortedMap> usedResources = new TreeMap>(); 767 | 768 | for (final Resource resource : mUsedResources) { 769 | final String type = resource.getType(); 770 | SortedMap typeMap = usedResources.get(type); 771 | 772 | if (typeMap == null) { 773 | typeMap = new TreeMap(); 774 | usedResources.put(type, typeMap); 775 | } 776 | 777 | typeMap.put(resource.getName(), resource); 778 | } 779 | 780 | // Ensure we only try to find resource types that exist in the map we just built 781 | final Map usedResourceTypes = new HashMap(usedResources.size()); 782 | 783 | for (final String type : usedResources.keySet()) { 784 | final ResourceType resourceType = sResourceTypes.get(type); 785 | if (resourceType != null) { 786 | usedResourceTypes.put(type, resourceType); 787 | } 788 | } 789 | 790 | findDeclaredPaths(null, mResDirectory, usedResourceTypes, usedResources); 791 | 792 | // Deal with resources from library projects 793 | final Set libraryProjectResources = getLibraryProjectResources(); 794 | 795 | /* 796 | * Since an app can override a library project resource, we cannot simply remove all resources that are defined in library projects. Instead, we must 797 | * only remove them if we cannot find a declaration of them in the current project. 798 | */ 799 | for (final Resource libraryResource : libraryProjectResources) { 800 | final SortedMap typedResources = unusedResources.get(libraryResource.getType()); 801 | 802 | if (typedResources != null) { 803 | final Resource appResource = typedResources.get(libraryResource.getName()); 804 | 805 | if (appResource != null && appResource.hasNoDeclaredPaths()) { 806 | typedResources.remove(libraryResource.getName()); 807 | mResources.remove(appResource); 808 | } 809 | } 810 | } 811 | 812 | final UsageMatrix usageMatrix = new UsageMatrix(mBaseDirectory, usedResources); 813 | usageMatrix.generateMatrices(); 814 | 815 | final int unusedResourceCount = mResources.size(); 816 | 817 | if (unusedResourceCount > 0) { 818 | System.out.println(unusedResourceCount + " unused resources were found:"); 819 | 820 | final SortedSet sortedResources = new TreeSet(mResources); 821 | 822 | for (final Resource resource : sortedResources) { 823 | System.out.println(resource); 824 | } 825 | 826 | System.out.println(); 827 | System.out.println("If any of the above resources are used, please submit your project as a test case so this application can be improved."); 828 | System.out.println(); 829 | System.out.println("This application does not maintain a dependency graph, so you should run it again after removing the above resources."); 830 | } else { 831 | System.out.println("No unused resources were detected."); 832 | System.out.println("If you know you have some unused resources, please submit your project as a test case so this application can be improved."); 833 | } 834 | } 835 | 836 | private void findPaths() { 837 | final File[] children = mBaseDirectory.listFiles(); 838 | 839 | if (children == null) { 840 | return; 841 | } 842 | 843 | for (final File file : children) { 844 | if (file.isDirectory()) { 845 | if (file.getName().equals("src")) { 846 | mSrcDirectory = file; 847 | } else if (file.getName().equals("res")) { 848 | mResDirectory = file; 849 | } else if (file.getName().equals("gen")) { 850 | mGenDirectory = file; 851 | } 852 | } else if (file.getName().equals("AndroidManifest.xml")) { 853 | mManifestFile = file; 854 | } 855 | } 856 | } 857 | 858 | private static String findPackageName(final File androidManifestFile) { 859 | String manifest = ""; 860 | 861 | try { 862 | manifest = FileUtilities.getFileContents(androidManifestFile); 863 | } catch (final IOException e) { 864 | e.printStackTrace(); 865 | } 866 | 867 | final Pattern pattern = Pattern.compile(""); 868 | final Matcher matcher = pattern.matcher(manifest); 869 | 870 | if (matcher.find()) { 871 | return matcher.group(1); 872 | } 873 | 874 | return null; 875 | } 876 | 877 | private static File findRJavaFile(final File baseDirectory, final String packageName) { 878 | final File rJava = new File(baseDirectory, packageName.replace('.', '/') + "/R.java"); 879 | 880 | if (rJava.exists()) { 881 | return rJava; 882 | } 883 | 884 | return null; 885 | } 886 | 887 | /** 888 | * Removes all resources declared in library projects. 889 | */ 890 | private Set getLibraryProjectResources() { 891 | final Set resources = new HashSet(); 892 | 893 | // Find the library projects 894 | final File projectPropertiesFile = new File(mBaseDirectory, "project.properties"); 895 | 896 | if (!projectPropertiesFile.exists()) { 897 | return resources; 898 | } 899 | 900 | List fileLines = new ArrayList(); 901 | try { 902 | fileLines = FileUtilities.getFileLines(projectPropertiesFile); 903 | } catch (final IOException e) { 904 | e.printStackTrace(); 905 | } 906 | 907 | final Pattern libraryProjectPattern = Pattern.compile("^android\\.library\\.reference\\.\\d+=(.*)$", Pattern.CASE_INSENSITIVE); 908 | 909 | final List libraryProjectPaths = new ArrayList(); 910 | 911 | for (final String line : fileLines) { 912 | final Matcher libraryProjectMatcher = libraryProjectPattern.matcher(line); 913 | 914 | if (libraryProjectMatcher.find()) { 915 | libraryProjectPaths.add(libraryProjectMatcher.group(1)); 916 | } 917 | } 918 | 919 | // We have the paths to the library projects, now we need their R.java files 920 | for (final String libraryProjectPath : libraryProjectPaths) { 921 | final File libraryProjectDirectory = new File(mBaseDirectory, libraryProjectPath); 922 | 923 | if (libraryProjectDirectory.exists() && libraryProjectDirectory.isDirectory()) { 924 | final String libraryProjectPackageName = findPackageName(new File(libraryProjectDirectory, "AndroidManifest.xml")); 925 | final File libraryProjectRJavaFile = findRJavaFile(new File(libraryProjectDirectory, "gen"), libraryProjectPackageName); 926 | 927 | // If a project has no resources, it will have no R.java 928 | if (libraryProjectRJavaFile != null) { 929 | try { 930 | resources.addAll(getResourceList(libraryProjectRJavaFile)); 931 | } catch (final IOException e) { 932 | e.printStackTrace(); 933 | } 934 | } 935 | } 936 | } 937 | 938 | return resources; 939 | } 940 | 941 | private static Set getResourceList(final File rJavaFile) throws IOException { 942 | final InputStream inputStream = new FileInputStream(rJavaFile); 943 | final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 944 | 945 | boolean done = false; 946 | 947 | final Set resources = new HashSet(); 948 | 949 | String type = ""; 950 | 951 | while (!done) { 952 | final String line = reader.readLine(); 953 | done = (line == null); 954 | 955 | if (line != null) { 956 | final Matcher typeMatcher = sResourceTypePattern.matcher(line); 957 | final Matcher nameMatcher = sResourceNamePattern.matcher(line); 958 | 959 | if (nameMatcher.find()) { 960 | resources.add(new Resource(type, nameMatcher.group(3))); 961 | } else if (typeMatcher.find()) { 962 | type = typeMatcher.group(1); 963 | } 964 | } 965 | } 966 | 967 | reader.close(); 968 | inputStream.close(); 969 | 970 | return resources; 971 | } 972 | 973 | private void searchFiles(final File parent, final File file, final FileType fileType) { 974 | if (file.isDirectory()) { 975 | for (final File child : file.listFiles()) { 976 | searchFiles(file, child, fileType); 977 | } 978 | } else if (file.getName().endsWith(fileType.getExtension())) { 979 | try { 980 | searchFile(parent, file, fileType); 981 | } catch (final IOException e) { 982 | System.err.println("There was a problem reading " + file.getAbsolutePath()); 983 | e.printStackTrace(); 984 | } 985 | } 986 | } 987 | 988 | private void searchFile(final File parent, final File file, final FileType fileType) throws IOException { 989 | final Set foundResources = new HashSet(); 990 | 991 | final String fileContents = FileUtilities.getFileContents(file); 992 | 993 | for (final Resource resource : mResources) { 994 | final Matcher matcher = fileType.getPattern(resource.getType(), resource.getName().replace("_", "[_\\.]")).matcher(fileContents); 995 | 996 | if (matcher.find()) { 997 | foundResources.add(resource); 998 | } else { 999 | final ResourceType type = sResourceTypes.get(resource.getType()); 1000 | 1001 | if (type != null && type.doesFileUseResource(parent, file.getName(), fileContents, resource.getName().replace("_", "[_\\.]"))) { 1002 | foundResources.add(resource); 1003 | } 1004 | } 1005 | } 1006 | 1007 | for (final Resource resource : foundResources) { 1008 | mUsedResources.add(resource); 1009 | mResources.remove(resource); 1010 | } 1011 | } 1012 | 1013 | private void findDeclaredPaths(final File parent, final File file, final Map resourceTypes, 1014 | final Map> resources) { 1015 | if (file.isDirectory()) { 1016 | for (final File child : file.listFiles()) { 1017 | if (!child.isHidden()) { 1018 | findDeclaredPaths(file, child, resourceTypes, resources); 1019 | } 1020 | } 1021 | } else { 1022 | if (!file.isHidden()) { 1023 | final String fileName = file.getName(); 1024 | 1025 | String fileContents = ""; 1026 | try { 1027 | fileContents = FileUtilities.getFileContents(file); 1028 | } catch (final IOException e) { 1029 | e.printStackTrace(); 1030 | } 1031 | 1032 | for (final ResourceType resourceType : resourceTypes.values()) { 1033 | final Map typeMap = resources.get(resourceType.getType()); 1034 | 1035 | if (typeMap != null) { 1036 | for (final Resource resource : typeMap.values()) { 1037 | if (resourceType.doesFileDeclareResource(parent, fileName, fileContents, resource.getName().replace("_", "[_\\.]"))) { 1038 | resource.addDeclaredPath(file.getAbsolutePath()); 1039 | 1040 | final String configuration = parent.getName(); 1041 | resource.addConfiguration(configuration); 1042 | } 1043 | } 1044 | } 1045 | } 1046 | } 1047 | } 1048 | } 1049 | } 1050 | --------------------------------------------------------------------------------