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 |
--------------------------------------------------------------------------------