Inline string resource values into code

This will inline resource strings that have a single value into code if the Resources:getString(int id) method is used

This is currently only inlining into code, I will follow up with inlining into xml and then implement a second round of tracing to (hopefully) eliminate the resource table entry.

Bug: b/311321134
Bug: b/287398085
Change-Id: Id5f5301a13a3d0d3a17da8a539ba0a0c7dfe9297
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6e8edf7..a72492a 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -906,8 +906,7 @@
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
-        shrinkerResult =
-            shrinker.shrinkModel(appView.getResourceShrinkerState().getR8ResourceShrinkerModel());
+        shrinkerResult = shrinker.shrinkModel(appView.getResourceAnalysisResult().getModel());
       } else {
         shrinkerResult = shrinker.run();
       }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 3e3fe4c..f806710 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.graph;
 
-import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.contexts.CompilationContext;
@@ -13,6 +12,7 @@
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
+import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis.ResourceAnalysisResult;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.graph.lens.InitClassLens;
@@ -149,7 +149,7 @@
 
   private SeedMapper applyMappingSeedMapper;
 
-  R8ResourceShrinkerState resourceShrinkerState = null;
+  private ResourceAnalysisResult resourceAnalysisResult = null;
 
   // When input has been (partially) desugared these are the classes which has been library
   // desugared. This information is populated in the IR converter.
@@ -871,12 +871,12 @@
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
 
-  public R8ResourceShrinkerState getResourceShrinkerState() {
-    return resourceShrinkerState;
+  public void setResourceAnalysisResult(ResourceAnalysisResult resourceAnalysisResult) {
+    this.resourceAnalysisResult = resourceAnalysisResult;
   }
 
-  public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
-    this.resourceShrinkerState = resourceShrinkerState;
+  public ResourceAnalysisResult getResourceAnalysisResult() {
+    return resourceAnalysisResult;
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
@@ -971,6 +971,9 @@
       setProguardCompatibilityActions(
           getProguardCompatibilityActions().withoutPrunedItems(prunedItems, timing));
     }
+    if (resourceAnalysisResult != null) {
+      resourceAnalysisResult.withoutPrunedItems(prunedItems, timing);
+    }
     if (hasRootSet()) {
       rootSet.pruneItems(prunedItems, timing);
     }
@@ -1175,6 +1178,17 @@
               new ThreadTask() {
                 @Override
                 public void run(Timing threadTiming) {
+                  appView.resourceAnalysisResult.rewrittenWithLens(lens, threadTiming);
+                }
+
+                @Override
+                public boolean shouldRun() {
+                  return appView.resourceAnalysisResult != null;
+                }
+              },
+              new ThreadTask() {
+                @Override
+                public void run(Timing threadTiming) {
                   appView.setRootSet(appView.rootSet().rewrittenWithLens(lens, threadTiming));
                 }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 92ceda8..39c4168 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -93,6 +93,7 @@
   public static final String androidMediaMediaDrmDescriptorString = "Landroid/media/MediaDrm;";
   public static final String androidMediaMediaMetadataRetrieverDescriptorString =
       "Landroid/media/MediaMetadataRetriever;";
+  public static final String androidResourcesDescriptorString = "Landroid/content/res/Resources;";
 
   /** Set of types that may be synthesized during compilation. */
   private final Set<DexType> possibleCompilerSynthesizedTypes = Sets.newIdentityHashSet();
@@ -643,6 +644,13 @@
       createStaticallyKnownType(androidMediaMediaDrmDescriptorString);
   public final DexType androidMediaMediaMetadataRetrieverType =
       createStaticallyKnownType(androidMediaMediaMetadataRetrieverDescriptorString);
+  public final DexType androidResourcesType =
+      createStaticallyKnownType(androidResourcesDescriptorString);
+  public final DexString androidResourcesGetStringName = createString("getString");
+  public final DexProto androidResourcesGetStringProto = createProto(stringType, intType);
+  public final DexMethod androidResourcesGetStringMethod =
+      createMethod(
+          androidResourcesType, androidResourcesGetStringProto, androidResourcesGetStringName);
 
   public final StringBuildingMethods stringBuilderMethods =
       new StringBuildingMethods(stringBuilderType);
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index d4ebe2f..ed61adb 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph.analysis;
 
 import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
 import com.android.tools.r8.AndroidResourceInput;
 import com.android.tools.r8.AndroidResourceInput.Kind;
 import com.android.tools.r8.ResourceException;
@@ -12,9 +13,12 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
@@ -23,8 +27,10 @@
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerWorklist;
+import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.Timing;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.IdentityHashMap;
@@ -34,18 +40,15 @@
 public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
 
   private final R8ResourceShrinkerState resourceShrinkerState;
-  private final Map<DexProgramClass, RClassFieldToValueStore> fieldToValueMapping =
-      new IdentityHashMap<>();
+  private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Enqueuer enqueuer;
 
-  @SuppressWarnings("UnusedVariable")
   private ResourceAccessAnalysis(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      Enqueuer enqueuer,
-      R8ResourceShrinkerState resourceShrinkerState) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     this.appView = appView;
-    this.resourceShrinkerState = resourceShrinkerState;
-    appView.setResourceShrinkerState(resourceShrinkerState);
+    this.enqueuer = enqueuer;
+    this.resourceShrinkerState = new R8ResourceShrinkerState();
     try {
       for (AndroidResourceInput androidResource :
           appView.options().androidResourceProvider.getAndroidResources()) {
@@ -62,14 +65,14 @@
   public static void register(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     if (enabled(appView, enqueuer)) {
-      enqueuer.registerFieldAccessAnalysis(
-          new ResourceAccessAnalysis(appView, enqueuer, new R8ResourceShrinkerState()));
+      enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
     }
   }
 
   @Override
   public void done(Enqueuer enqueuer) {
-    appView.setResourceShrinkerState(resourceShrinkerState);
+    appView.setResourceAnalysisResult(
+        new ResourceAnalysisResult(resourceShrinkerState, fieldToValueMapping));
     EnqueuerFieldAccessAnalysis.super.done(enqueuer);
   }
 
@@ -93,13 +96,15 @@
       return;
     }
     if (getMaybeCachedIsRClass(resolvedField.getHolder())) {
-      DexProgramClass holderType = resolvedField.getHolder();
+      DexType holderType = resolvedField.getHolderType();
       if (!fieldToValueMapping.containsKey(holderType)) {
         populateRClassValues(resolvedField);
       }
       assert fieldToValueMapping.containsKey(holderType);
       RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
       IntList integers = rClassFieldToValueStore.valueMapping.get(field);
+      enqueuer.applyMinimumKeepInfoWhenLive(
+          resolvedField, KeepFieldInfo.newEmptyJoiner().disallowOptimization());
       for (Integer integer : integers) {
         resourceShrinkerState.trace(integer);
       }
@@ -161,7 +166,7 @@
       rClassValueBuilder.addMapping(staticPut.getField(), values);
     }
 
-    fieldToValueMapping.put(field.getHolder(), rClassValueBuilder.build());
+    fieldToValueMapping.put(field.getHolderType(), rClassValueBuilder.build());
   }
 
   private final Map<DexProgramClass, Boolean> cachedClassLookups = new IdentityHashMap<>();
@@ -189,13 +194,103 @@
     return isRClass;
   }
 
+  public static class ResourceAnalysisResult {
+
+    private final R8ResourceShrinkerState resourceShrinkerState;
+    private Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap;
+
+    private ResourceAnalysisResult(
+        R8ResourceShrinkerState resourceShrinkerState,
+        Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap) {
+      this.resourceShrinkerState = resourceShrinkerState;
+      this.rClassFieldToValueStoreMap = rClassFieldToValueStoreMap;
+    }
+
+    public R8ResourceShrinkerModel getModel() {
+      return resourceShrinkerState.getR8ResourceShrinkerModel();
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    public void rewrittenWithLens(GraphLens lens, Timing timing) {
+      Map<DexType, DexType> changed = new IdentityHashMap<>();
+      for (DexType dexType : rClassFieldToValueStoreMap.keySet()) {
+        DexType rewritten = lens.lookupClassType(dexType);
+        if (rewritten != dexType) {
+          changed.put(dexType, rewritten);
+        }
+      }
+      if (changed.size() > 0) {
+        Map<DexType, RClassFieldToValueStore> rewrittenMap = new IdentityHashMap<>();
+        rClassFieldToValueStoreMap.forEach(
+            (type, map) -> {
+              rewrittenMap.put(changed.getOrDefault(type, type), map);
+              map.rewrittenWithLens(lens);
+            });
+        rClassFieldToValueStoreMap = rewrittenMap;
+      }
+    }
+
+    public void withoutPrunedItems(PrunedItems prunedItems, Timing timing) {
+      rClassFieldToValueStoreMap.keySet().removeIf(prunedItems::isRemoved);
+      rClassFieldToValueStoreMap.values().forEach(store -> store.pruneItems(prunedItems));
+    }
+
+    public String getSingleStringValueForField(ProgramField programField) {
+      RClassFieldToValueStore rClassFieldToValueStore =
+          rClassFieldToValueStoreMap.get(programField.getHolderType());
+      if (rClassFieldToValueStore == null) {
+        return null;
+      }
+      if (!rClassFieldToValueStore.hasField(programField.getReference())) {
+        return null;
+      }
+      return getModel()
+          .getStringResourcesWithSingleValue()
+          .get(rClassFieldToValueStore.getResourceId(programField.getReference()));
+    }
+  }
+
   private static class RClassFieldToValueStore {
-    private final Map<DexField, IntList> valueMapping;
+    private Map<DexField, IntList> valueMapping;
 
     private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
       this.valueMapping = valueMapping;
     }
 
+    boolean hasField(DexField field) {
+      return valueMapping.containsKey(field);
+    }
+
+    void pruneItems(PrunedItems prunedItems) {
+      valueMapping.keySet().removeIf(prunedItems::isRemoved);
+    }
+
+    int getResourceId(DexField field) {
+      IntList integers = valueMapping.get(field);
+      assert integers.size() == 1;
+      return integers.get(0);
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    public void rewrittenWithLens(GraphLens lens) {
+      Map<DexField, DexField> changed = new IdentityHashMap<>();
+      valueMapping
+          .keySet()
+          .forEach(
+              dexField -> {
+                DexField rewritten = lens.lookupField(dexField);
+                if (rewritten != dexField) {
+                  changed.put(dexField, rewritten);
+                }
+              });
+      if (changed.size() > 0) {
+        Map<DexField, IntList> rewrittenMapping = new IdentityHashMap<>();
+        valueMapping.forEach(
+            (key, value) -> rewrittenMapping.put(changed.getOrDefault(key, key), value));
+        valueMapping = rewrittenMapping;
+      }
+    }
+
     public static class Builder {
       private final Map<DexField, IntList> valueMapping = new IdentityHashMap<>();
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
index acf1bce..11a3951 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
@@ -11,7 +11,8 @@
 
   @Override
   public boolean canMerge(DexProgramClass program) {
-    return !program.getSimpleName().startsWith("R$");
+    String simpleName = program.getSimpleName();
+    return !simpleName.startsWith("R$") && !simpleName.contains("$R$");
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index f47e771..1dc3ca2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -50,6 +50,10 @@
     register(new ObjectsMethodOptimizer(appView));
     register(new StringBuilderMethodOptimizer(appView));
     register(new StringMethodOptimizer(appView));
+    if (appView.enableWholeProgramOptimizations()
+        && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()) {
+      register(new ResourcesMemberOptimizer(appView));
+    }
     if (appView.enableWholeProgramOptimizations()) {
       // Subtyping is required to prove the enum class is a subtype of java.lang.Enum.
       register(new EnumMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
new file mode 100644
index 0000000..1246516
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class ResourcesMemberOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+  private Optional<Boolean> allowStringInlining = Optional.empty();
+
+  ResourcesMemberOptimizer(AppView<?> appView) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private synchronized boolean allowInliningOfGetStringCalls() {
+    if (allowStringInlining.isPresent()) {
+      return allowStringInlining.get();
+    }
+    // TODO(b/312406163): Allow androidx classes that overwrite this, but don't change the value
+    // or have side effects.
+    allowStringInlining = Optional.of(true);
+    Map<DexClass, Boolean> cachedResults = new IdentityHashMap<>();
+    TopDownClassHierarchyTraversal.forProgramClasses(appView.withClassHierarchy())
+        .visit(
+            appView.appInfo().classes(),
+            clazz -> {
+              if (isResourcesSubtype(cachedResults, clazz)) {
+                DexEncodedMethod dexEncodedMethod =
+                    clazz.lookupMethod(
+                        dexItemFactory.androidResourcesGetStringProto,
+                        dexItemFactory.androidResourcesGetStringName);
+                if (dexEncodedMethod != null) {
+                  // TODO(b/312695444): Break out of traversal when supported.
+                  allowStringInlining = Optional.of(false);
+                }
+              }
+            });
+    return allowStringInlining.get();
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private boolean isResourcesSubtype(Map<DexClass, Boolean> cachedLookups, DexClass dexClass) {
+    Boolean cachedValue = cachedLookups.get(dexClass);
+    if (cachedValue != null) {
+      return cachedValue;
+    }
+    if (dexClass.type == dexItemFactory.androidResourcesType) {
+      return true;
+    }
+    if (dexClass.type == dexItemFactory.objectType) {
+      return false;
+    }
+
+    if (dexClass.superType != null) {
+      DexClass superClass = appView.definitionFor(dexClass.superType);
+      if (superClass != null) {
+        boolean superIsResourcesSubtype = isResourcesSubtype(cachedLookups, superClass);
+        cachedLookups.put(dexClass, superIsResourcesSubtype);
+        return superIsResourcesSubtype;
+      }
+    }
+    cachedLookups.put(dexClass, false);
+    return false;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.androidResourcesType;
+  }
+
+  @Override
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    if (allowInliningOfGetStringCalls()
+        && singleTarget
+            .getReference()
+            .isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod)) {
+      maybeInlineGetString(code, instructionIterator, invoke, affectedValues);
+    }
+    return instructionIterator;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private void maybeInlineGetString(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      AffectedValues affectedValues) {
+    if (invoke.isInvokeVirtual()) {
+      InvokeVirtual invokeVirtual = invoke.asInvokeVirtual();
+      DexMethod invokedMethod = invokeVirtual.getInvokedMethod();
+      assert invokedMethod.isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod);
+      assert invoke.inValues().size() == 2;
+      Instruction valueDefinition = invoke.getLastArgument().definition;
+      if (valueDefinition != null && valueDefinition.isStaticGet()) {
+        DexField field = valueDefinition.asStaticGet().getField();
+        FieldResolutionResult fieldResolutionResult =
+            appView.appInfo().resolveField(field, code.context());
+        ProgramField resolvedField = fieldResolutionResult.getProgramField();
+        if (resolvedField != null) {
+          String singleStringValueForField =
+              appView.getResourceAnalysisResult().getSingleStringValueForField(resolvedField);
+          if (singleStringValueForField != null) {
+            DexString value = dexItemFactory.createString(singleStringValueForField);
+            instructionIterator.replaceCurrentInstructionWithConstString(
+                appView, code, value, affectedValues);
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index c8d9806..d1a2dda 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3778,7 +3778,7 @@
     }
   }
 
-  private void applyMinimumKeepInfoWhenLive(
+  public void applyMinimumKeepInfoWhenLive(
       ProgramField field, KeepFieldInfo.Joiner minimumKeepInfo) {
     applyMinimumKeepInfoWhenLive(field, minimumKeepInfo, EnqueuerEvent.unconditional());
   }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 8273c96..e0cca4f 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -4,7 +4,11 @@
 
 package com.android.build.shrinker.r8integration;
 
+import com.android.aapt.Resources.ConfigValue;
+import com.android.aapt.Resources.Entry;
+import com.android.aapt.Resources.Item;
 import com.android.aapt.Resources.ResourceTable;
+import com.android.aapt.Resources.Value;
 import com.android.build.shrinker.NoDebugReporter;
 import com.android.build.shrinker.ResourceShrinkerModel;
 import com.android.build.shrinker.ResourceTableUtilKt;
@@ -16,7 +20,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class R8ResourceShrinkerState {
 
@@ -38,12 +44,17 @@
   }
 
   public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
+    private final Map<Integer, String> stringResourcesWithSingleValue = new HashMap<>();
 
     public R8ResourceShrinkerModel(
         ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
       super(debugReporter, supportMultipackages);
     }
 
+    public Map<Integer, String> getStringResourcesWithSingleValue() {
+      return stringResourcesWithSingleValue;
+    }
+
     // Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
     void instantiateFromResourceTable(InputStream inputStream) {
       try {
@@ -60,14 +71,34 @@
           .forEachRemaining(
               entryWrapper -> {
                 ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
+                Entry entry = entryWrapper.getEntry();
+                int entryId = entryWrapper.getId();
+                recordSingleValueResources(resourceType, entry, entryId);
                 if (resourceType != ResourceType.STYLEABLE) {
                   this.addResource(
                       resourceType,
                       entryWrapper.getPackageName(),
-                      ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
-                      entryWrapper.getId());
+                      ResourcesUtil.resourceNameToFieldName(entry.getName()),
+                      entryId);
                 }
               });
     }
+
+    private void recordSingleValueResources(ResourceType resourceType, Entry entry, int entryId) {
+      if (!entry.hasOverlayableItem() && entry.getConfigValueList().size() == 1) {
+        if (resourceType == ResourceType.STRING) {
+          ConfigValue configValue = entry.getConfigValue(0);
+          if (configValue.hasValue()) {
+            Value value = configValue.getValue();
+            if (value.hasItem()) {
+              Item item = value.getItem();
+              if (item.hasStr()) {
+                stringResourcesWithSingleValue.put(entryId, item.getStr().getValue());
+              }
+            }
+          }
+        }
+      }
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 7ae3463..bbab1ac 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -291,6 +291,11 @@
     return self();
   }
 
+  public T enableOptimizedShrinking() {
+    builder.setResourceShrinkerConfiguration(b -> b.enableOptimizedShrinkingWithR8().build());
+    return self();
+  }
+
   /**
    * Allow info, warning, and error diagnostics.
    *
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 0ee66c7..785a049 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -10,9 +10,12 @@
 import com.android.aapt.Resources.ConfigValue;
 import com.android.aapt.Resources.Item;
 import com.android.aapt.Resources.ResourceTable;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.transformers.ClassTransformer;
 import com.android.tools.r8.transformers.MethodTransformer;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -34,12 +37,16 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
 public class AndroidResourceTestingUtils {
+  private static final String RESOURCES_DESCRIPTOR =
+      TestBase.descriptor(com.android.tools.r8.androidresources.Resources.class);
 
   enum RClassType {
     STRING,
@@ -53,6 +60,32 @@
     }
   }
 
+  public static Path resourcesClassAsDex(TemporaryFolder temp) throws Exception {
+    return TestBase.testForD8(temp, Backend.DEX)
+        .addProgramClassFileData(resouresClassAsJavaClass())
+        .compile()
+        .writeToZip();
+  }
+
+  public static byte[] resouresClassAsJavaClass() throws IOException {
+    return transformer(com.android.tools.r8.androidresources.Resources.class)
+        .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMethodInstructions(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMembers(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
+  public static byte[] transformResourcesReferences(Class clazz) throws IOException {
+    return transformer(clazz)
+        .replaceClassDescriptorInMethodInstructions(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMembers(
+            RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
   private static String rClassWithoutNamespaceAndOuter(Class clazz) {
     return rClassWithoutNamespaceAndOuter(clazz.getName());
   }
@@ -223,6 +256,8 @@
   public static class AndroidTestResourceBuilder {
     private String manifest;
     private final Map<String, String> stringValues = new TreeMap<>();
+    private final Set<String> stringValuesWithExtraLanguage = new TreeSet<>();
+    private final Map<String, String> overlayableValues = new TreeMap<>();
     private final Map<String, Integer> styleables = new TreeMap<>();
     private final Map<String, byte[]> drawables = new TreeMap<>();
     private final Map<String, String> xmlFiles = new TreeMap<>();
@@ -282,6 +317,16 @@
       return this;
     }
 
+    AndroidTestResourceBuilder addExtraLanguageString(String name) {
+      stringValuesWithExtraLanguage.add(name);
+      return this;
+    }
+
+    AndroidTestResourceBuilder setOverlayableFor(String type, String name) {
+      overlayableValues.put(type, name);
+      return this;
+    }
+
     AndroidTestResourceBuilder setPackageId(int packageId) {
       this.packageId = packageId;
       return this;
@@ -298,7 +343,16 @@
       Path resFolder = temp.newFolder("res").toPath();
       Path valuesFolder = temp.newFolder("res", "values").toPath();
       if (stringValues.size() > 0) {
-        FileUtils.writeTextFile(valuesFolder.resolve("strings.xml"), createStringResourceXml());
+        FileUtils.writeTextFile(
+            valuesFolder.resolve("strings.xml"), createStringResourceXml(false));
+        if (stringValuesWithExtraLanguage.size() > 0) {
+          Path languageValues = temp.newFolder("res", "values-da").toPath();
+          FileUtils.writeTextFile(
+              languageValues.resolve("strings.xml"), createStringResourceXml(true));
+        }
+      }
+      if (overlayableValues.size() > 0) {
+        FileUtils.writeTextFile(valuesFolder.resolve("overlayable.xml"), createOverlayableXml());
       }
       if (styleables.size() > 0) {
         FileUtils.writeTextFile(
@@ -401,11 +455,30 @@
           new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output);
     }
 
-    private String createStringResourceXml() {
+    private String createOverlayableXml() {
       StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+
+      stringBuilder.append("<overlayable name=\"OurOverlayables\">\n");
+      stringBuilder.append("<policy type=\"public\">\n");
+      overlayableValues.forEach(
+          (type, name) ->
+              stringBuilder.append("<item type=\"" + type + "\" name=\"" + name + "\" />\n"));
+      stringBuilder.append("</policy>\n");
+      stringBuilder.append("</overlayable>");
+      stringBuilder.append("</resources>");
+      return stringBuilder.toString();
+    }
+
+    private String createStringResourceXml(boolean nonDefaultLanguage) {
+      StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+      String languagePostFix = nonDefaultLanguage ? "_da" : "";
       stringValues.forEach(
-          (name, value) ->
-              stringBuilder.append("<string name=\"" + name + "\">" + value + "</string>\n"));
+          (name, value) -> {
+            if (!nonDefaultLanguage || stringValuesWithExtraLanguage.contains(name)) {
+              stringBuilder.append(
+                  "<string name=\"" + name + "\">" + value + languagePostFix + "</string>\n");
+            }
+          });
       stringBuilder.append("</resources>");
       return stringBuilder.toString();
     }
@@ -496,4 +569,5 @@
   // The XML document <x/> as a proto packed with AAPT2
   public static final byte[] TINY_PROTO_XML =
       new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+
 }
diff --git a/src/test/java/com/android/tools/r8/androidresources/Resources.java b/src/test/java/com/android/tools/r8/androidresources/Resources.java
new file mode 100644
index 0000000..ef01a99
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/Resources.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.androidresources;
+
+public class Resources {
+  public static String GET_STRING_VALUE = "GET_STRING_VALUE";
+
+  // Returns the GET_STRING_VALUE  to be able to distinguish resource inlined values from values
+  // we get from this call (i.e., not inlined). Inlined values are the actual values from the
+  // resource table.
+  public String getString(int id) {
+    return GET_STRING_VALUE;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
new file mode 100644
index 0000000..bd97b91
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
@@ -0,0 +1,173 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestResourceInlining extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean optimize;
+
+  @Parameter(2)
+  public boolean addResourcesSubclass;
+
+  @Parameters(name = "{0}, optimize: {1}, addResourcesSubclass: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class)
+        .setOverlayableFor("string", "overlayable")
+        .addExtraLanguageString("bar")
+        .build(temp);
+  }
+
+  public static Path getAndroidResourcesClassInJar(TemporaryFolder temp) throws Exception {
+    byte[] androidResourcesClass =
+        transformer(Resources.class)
+            .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+            .transform();
+    return testForR8(temp, Backend.DEX)
+        .addProgramClassFileData(androidResourcesClass)
+        .compile()
+        .writeToZip();
+  }
+
+  public byte[] getResourcesSubclass() throws IOException {
+    return transformer(ResourcesSubclass.class)
+        .setSuper(DexItemFactory.androidResourcesDescriptorString)
+        .replaceClassDescriptorInMethodInstructions(
+            descriptor(Resources.class), DexItemFactory.androidResourcesDescriptorString)
+        .transform();
+  }
+
+  @Test
+  public void testR8Optimized() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClassFileData(
+            AndroidResourceTestingUtils.transformResourcesReferences(FooBar.class))
+        .applyIf(
+            addResourcesSubclass,
+            builder -> {
+              builder.addProgramClassFileData(getResourcesSubclass());
+              builder.addKeepClassRules(ResourcesSubclass.class);
+            })
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
+        .addRunClasspathFiles(AndroidResourceTestingUtils.resourcesClassAsDex(temp))
+        .compile()
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              // We should eventually remove this when optimizing
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+              // Has multiple values, don't inline
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              // Has overlayable value, don't inline
+              resourceTableInspector.assertContainsResourceWithName("string", "bar");
+              resourceTableInspector.assertDoesNotContainResourceWithName(
+                  "string", "unused_string");
+            })
+        .inspect(
+            inspector -> {
+              // We should have removed one of the calls to getString if we are optimizing.
+              MethodSubject mainMethodSubject = inspector.clazz(FooBar.class).mainMethod();
+              assertThat(mainMethodSubject, isPresent());
+              assertEquals(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .filter(InstructionSubject::isInvokeVirtual)
+                      .filter(
+                          i ->
+                              i.getMethod()
+                                  .holder
+                                  .descriptor
+                                  .toString()
+                                  .equals(DexItemFactory.androidResourcesDescriptorString))
+                      .count(),
+                  optimize && !addResourcesSubclass ? 2 : 3);
+            })
+        .run(parameters.getRuntime(), FooBar.class)
+        .applyIf(
+            optimize && !addResourcesSubclass,
+            result -> {
+              result.assertSuccessWithOutputLines(
+                  "foo", Resources.GET_STRING_VALUE, Resources.GET_STRING_VALUE);
+            })
+        .applyIf(
+            !optimize || addResourcesSubclass,
+            result -> {
+              result.assertSuccessWithOutputLines(
+                  Resources.GET_STRING_VALUE,
+                  Resources.GET_STRING_VALUE,
+                  Resources.GET_STRING_VALUE);
+            });
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      Resources resources = new Resources();
+      // Ensure that we correctly handle the out value propagation
+      String s = resources.getString(R.string.foo);
+      String t = "X";
+      String u = System.currentTimeMillis() > 0 ? s : t;
+      System.out.println(u);
+      System.out.println(resources.getString(R.string.bar));
+      System.out.println(resources.getString(R.string.overlayable));
+    }
+  }
+
+  public static class ResourcesSubclass extends Resources {
+
+    @Override
+    public String getString(int id) {
+      System.out.println("foobar");
+      return super.getString(id);
+    }
+  }
+
+  public static class R {
+
+    public static class string {
+      public static int foo;
+      public static int bar;
+      public static int overlayable;
+      public static int unused_string;
+    }
+  }
+}