|  | /* | 
|  | * Copyright (C) 2022 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package com.android.build.shrinker; | 
|  |  | 
|  | import com.android.aapt.Resources.ResourceTable; | 
|  | import com.android.annotations.NonNull; | 
|  | import com.android.annotations.Nullable; | 
|  | import com.android.build.shrinker.obfuscation.ObfuscatedClasses; | 
|  | import com.android.ide.common.resources.usage.ResourceStore; | 
|  | import com.android.ide.common.resources.usage.ResourceUsageModel.Resource; | 
|  | import com.android.resources.ResourceType; | 
|  | import com.google.common.collect.Maps; | 
|  | import com.google.common.collect.Sets; | 
|  | import java.io.IOException; | 
|  | import java.io.UncheckedIOException; | 
|  | import java.nio.file.Files; | 
|  | import java.nio.file.Path; | 
|  | import java.util.Map; | 
|  | import java.util.Set; | 
|  | import java.util.stream.Collectors; | 
|  |  | 
|  | /** | 
|  | * Represents resource shrinker state that is shared between shrinker stages ResourcesGatherer, | 
|  | * ObfuscationMappingsRecorder, ResourceUsageRecorder, ResourcesGraphBuilder and each stage | 
|  | * contributes an information via changing its state. | 
|  | */ | 
|  | public class ResourceShrinkerModel { | 
|  |  | 
|  | private final ShrinkerDebugReporter debugReporter; | 
|  |  | 
|  | private final ResourceStore resourceStore; | 
|  | private ObfuscatedClasses obfuscatedClasses = ObfuscatedClasses.NO_OBFUSCATION; | 
|  |  | 
|  | private final Set<String> strings = Sets.newHashSetWithExpectedSize(300); | 
|  |  | 
|  | private final Map<String, ResourceTable> resourceTableCache = Maps.newHashMap(); | 
|  |  | 
|  | private boolean foundGetIdentifier = false; | 
|  | private boolean foundWebContent = false; | 
|  |  | 
|  | public ResourceShrinkerModel( | 
|  | ShrinkerDebugReporter debugReporter, boolean supportMultipackages) { | 
|  | this.debugReporter = debugReporter; | 
|  | resourceStore = new ResourceStore(supportMultipackages); | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | public ShrinkerDebugReporter getDebugReporter() { | 
|  | return debugReporter; | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | public ResourceStore getResourceStore() { | 
|  | return resourceStore; | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | public ObfuscatedClasses getObfuscatedClasses() { | 
|  | return obfuscatedClasses; | 
|  | } | 
|  |  | 
|  | /** Reports recorded mappings from obfuscated classes to original classes */ | 
|  | public void setObfuscatedClasses(@NonNull ObfuscatedClasses obfuscatedClasses) { | 
|  | this.obfuscatedClasses = obfuscatedClasses; | 
|  | } | 
|  |  | 
|  | /** Adds a new gathered resource to model. */ | 
|  | @NonNull | 
|  | public Resource addResource( | 
|  | @NonNull ResourceType type, | 
|  | @Nullable String packageName, | 
|  | @NonNull String name, | 
|  | @Nullable String value) { | 
|  | int intValue = -1; | 
|  | try { | 
|  | intValue = value != null ? Integer.decode(value) : -1; | 
|  | } catch (NumberFormatException e) { | 
|  | // ignore | 
|  | } | 
|  | return addResource(type, packageName, name, intValue); | 
|  | } | 
|  |  | 
|  | /** Adds a new gathered resource to model. */ | 
|  | @NonNull | 
|  | public Resource addResource( | 
|  | @NonNull ResourceType type, | 
|  | @Nullable String packageName, | 
|  | @NonNull String name, | 
|  | int value) { | 
|  | return resourceStore.addResource(new Resource(packageName, type, name, value)); | 
|  | } | 
|  |  | 
|  | /** Adds string constant found in code to strings pool. */ | 
|  | public void addStringConstant(@NonNull String string) { | 
|  | strings.add(string); | 
|  | } | 
|  |  | 
|  | /** Returns is reference to {@code Resources#getIdentifier} is found in code. */ | 
|  | public boolean isFoundGetIdentifier() { | 
|  | return foundGetIdentifier; | 
|  | } | 
|  |  | 
|  | /** Reports that reference to {@code Resources#getIdentifier} is found in code. */ | 
|  | public void setFoundGetIdentifier(boolean foundGetIdentifier) { | 
|  | this.foundGetIdentifier = foundGetIdentifier; | 
|  | } | 
|  |  | 
|  | /** Returns is web content is found in code. */ | 
|  | public boolean isFoundWebContent() { | 
|  | return foundWebContent; | 
|  | } | 
|  |  | 
|  | /** Reports that reference to web content is found in code. */ | 
|  | public void setFoundWebContent(boolean foundWebContent) { | 
|  | this.foundWebContent = foundWebContent; | 
|  | } | 
|  |  | 
|  | /** Returns all string constants gathered from compiled classes. */ | 
|  | public Set<String> getStrings() { | 
|  | return strings; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Mark resources that match string constants as reachable in case invocation of | 
|  | * {@code Resources#getIdentifier} or web content is found in code and safe mode in enabled. | 
|  | */ | 
|  | public void keepPossiblyReferencedResources() { | 
|  | if (strings.isEmpty() | 
|  | || !resourceStore.getSafeMode() | 
|  | || (!foundGetIdentifier && !foundWebContent)) { | 
|  | // No calls to android.content.res.Resources#getIdentifier; or user specifically asked | 
|  | // for us not to guess resources to keep | 
|  | return; | 
|  | } | 
|  |  | 
|  | debugReporter.debug(() -> "" | 
|  | + "android.content.res.Resources#getIdentifier present: " + foundGetIdentifier + "\n" | 
|  | + "Web content present: " + foundWebContent + "\n" | 
|  | + "Referenced Strings:\n" | 
|  | + strings.stream() | 
|  | .map(s -> s.trim().replace("\n", "\\n")) | 
|  | .filter(s -> !s.isEmpty()) | 
|  | .map(s -> s.length() > 40 ? s.substring(0, 37) + "..." : s) | 
|  | .collect(Collectors.joining("\n")) | 
|  | ); | 
|  |  | 
|  | new PossibleResourcesMarker(debugReporter, resourceStore, strings, foundWebContent) | 
|  | .markPossibleResourcesReachable(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Reads resource table from specified path and stores it in cache to be able to reuse it if | 
|  | * the same resource table is requested by another unit. | 
|  | */ | 
|  | public ResourceTable readResourceTable(Path resourceTablePath) { | 
|  | return resourceTableCache.computeIfAbsent(resourceTablePath.toString(), (path) -> { | 
|  | try { | 
|  | return ResourceTable.parseFrom(Files.readAllBytes(resourceTablePath)); | 
|  | } catch (IOException e) { | 
|  | throw new UncheckedIOException(e); | 
|  | } | 
|  | }); | 
|  | } | 
|  | } |