/* * Copyright 2017-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.android.resources; import com.facebook.buck.util.MoreCollectors; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.stream.IntStream; public class UsedResourcesFinder { interface ApkContentProvider { ResourceTable getResourceTable(); ResourcesXml getXml(String path); boolean hasFile(String path); } public static ResourceClosure computeClosure( ApkContentProvider apkContentProvider, Iterable<String> rootFiles, Iterable<Integer> rootIds) { State state = new State(apkContentProvider, rootFiles, rootIds); state.process(); return new ResourceClosure( state .processedFiles .stream() .filter(apkContentProvider::hasFile) .collect(MoreCollectors.toImmutableSet()), state.processedIds); } public static ResourceClosure computePrimaryApkClosure(ApkContentProvider apkContentProvider) { // The Android framework (and other apps) have easy access to the values in the manifest and // anything reachable from there. Also, the framework needs access to animation references to // drive some animations (requested by the app itself). ImmutableList.Builder<Integer> rootIds = ImmutableList.builder(); ResTablePackage resPackage = apkContentProvider.getResourceTable().getPackage(); for (ResTableTypeSpec spec : resPackage.getTypeSpecs()) { if (spec.getResourceTypeName(resPackage).equals("anim")) { int startId = (ResTablePackage.APP_PACKAGE_ID << 24) | (spec.getResourceType() << 16); rootIds.addAll(IntStream.range(startId, startId + spec.getEntryCount())::iterator); } } return computeClosure( apkContentProvider, ImmutableList.of("AndroidManifest.xml"), rootIds.build()); } public static class ResourceClosure { final Set<String> files; final Map<Integer, SortedSet<Integer>> idsByType; public ResourceClosure(Set<String> files, Map<Integer, SortedSet<Integer>> idsByType) { this.files = files; this.idsByType = idsByType; } } private UsedResourcesFinder() {} private static class State { final ApkContentProvider apkContent; final Map<Integer, SortedSet<Integer>> processedIds; final Map<Integer, SortedSet<Integer>> idsToProcess; final List<String> xmlToProcess; final Set<String> processedFiles; public State( ApkContentProvider apkContentProvider, Iterable<String> rootFiles, Iterable<Integer> rootIds) { this.apkContent = apkContentProvider; processedIds = new HashMap<>(); idsToProcess = new HashMap<>(); processedFiles = new HashSet<>(); xmlToProcess = new ArrayList<>(); rootFiles.forEach(this::addPossibleFileToExtract); rootIds.forEach(this::addIdToProcess); } void process() { while (!idsToProcess.isEmpty() || !xmlToProcess.isEmpty()) { processXml(); processIds(); } } void addIdToProcess(int id) { int pkg = id >> 24; if (pkg == ResTablePackage.APP_PACKAGE_ID) { int type = (id >> 16) & 0xFF; int k = id & 0xFFFF; if (!processedIds.containsKey(type)) { processedIds.put(type, Sets.newTreeSet()); idsToProcess.put(type, Sets.newTreeSet()); } Set<Integer> processedIdsForType = Preconditions.checkNotNull(processedIds.get(type)); if (!processedIdsForType.contains(k)) { processedIdsForType.add(k); Preconditions.checkNotNull(idsToProcess.get(type)).add(k); } } } void processIds() { ImmutableMap<Integer, SortedSet<Integer>> ids = ImmutableMap.copyOf(idsToProcess); idsToProcess.clear(); iterateArsc(ids); } void iterateArsc(ImmutableMap<Integer, SortedSet<Integer>> ids) { ResTablePackage resPackage = apkContent.getResourceTable().getPackage(); StringPool strings = apkContent.getResourceTable().getStrings(); ids.forEach( (k, v) -> { ResTableTypeSpec spec = resPackage.getTypeSpec(k); String resourceTypeName = spec.getResourceTypeName(resPackage); int[] idsToVisit = v.stream().mapToInt(i -> i).toArray(); spec.visitReferences(idsToVisit, this::addIdToProcess); if (!resourceTypeName.equals("string") && !resourceTypeName.equals("id")) { spec.visitStringReferences( idsToVisit, (stringRef) -> addPossibleFileToExtract(strings.getString(stringRef))); } }); } private void addPossibleFileToExtract(String val) { if (!processedFiles.contains(val)) { processedFiles.add(val); if (val.endsWith(".xml") && apkContent.hasFile(val)) { xmlToProcess.add(val); } } } void processXml() { // This doesn't add new xml to process. xmlToProcess.forEach(s -> apkContent.getXml(s).visitReferences(this::addIdToProcess)); xmlToProcess.clear(); } } }