Load typefaces via Consumer in Piet Android

TextElementAdapter still handles typeface fallbacks when the host calls consumer.accept(null).

I changed the missing fonts error to be called as a warning, since it is still be able to load a default font. I also updated it so that it's only called when no typefaces can be loaded, which is what is specified in errors.proto. That's also the behavior errors.proto specifies for image loading.

I had to update all of the font/typeface tests, since getTypeface() is a void method now that we're using a consumer.

PiperOrigin-RevId: 244713868
Change-Id: I14246f5a91f071e616811843615e48edb14e00e9
diff --git a/src/main/java/com/google/android/libraries/feed/piet/TextElementAdapter.java b/src/main/java/com/google/android/libraries/feed/piet/TextElementAdapter.java
index ed34181..bfa5190 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/TextElementAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/TextElementAdapter.java
@@ -26,6 +26,7 @@
 import android.view.Gravity;
 import android.view.View;
 import android.widget.TextView;
+import com.google.android.libraries.feed.common.functional.Consumer;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.piet.AdapterFactory.AdapterKeySupplier;
 import com.google.android.libraries.feed.piet.DebugLogger.MessageType;
@@ -249,6 +250,179 @@
     return intWithRegularRounding;
   }
 
+  @VisibleForTesting
+  // LINT.IfChange
+  void setValuesUsedInRecyclerKey(TextElementKey fontKey, FrameContext frameContext) {
+    TextView textView = getBaseView();
+    textView.setTextSize(fontKey.getSize());
+    if (!fontKey.typefaces.isEmpty()) {
+      FontDetails fontDetails =
+          new FontDetails(fontKey.typefaces, fontKey.isItalic(), frameContext);
+      loadFont(textView, fontDetails);
+    } else {
+      makeFontItalic(textView, fontKey.isItalic());
+    }
+  }
+
+  private void loadFont(TextView textView, FontDetails fontDetails) {
+    StylesProto.Typeface typeface = fontDetails.getTypefaceToLoad();
+    if (typeface == null) {
+      fontDetails
+          .getFrameContext()
+          .reportMessage(
+              MessageType.WARNING,
+              ErrorCode.ERR_MISSING_FONTS,
+              "Could not load specified typefaces.");
+      // We didn't load a typeface, but we can at least respect italicization.
+      makeFontItalic(textView, fontDetails.isItalic());
+      return;
+    }
+    switch (typeface.getTypefaceSpecifierCase()) {
+      case COMMON_TYPEFACE:
+        loadCommonTypeface(typeface.getCommonTypeface(), fontDetails, textView);
+        break;
+      case CUSTOM_TYPEFACE:
+        loadCustomTypeface(typeface.getCustomTypeface(), fontDetails, textView);
+        break;
+      default:
+        // do nothing
+    }
+  }
+
+  /** Load one of the typefaces from the {@link CommonTypeface} enum. */
+  private void loadCommonTypeface(
+      CommonTypeface commonTypeface, FontDetails fontDetails, TextView textView) {
+    switch (commonTypeface) {
+      case PLATFORM_DEFAULT_LIGHT:
+        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_light);
+        break;
+      case PLATFORM_DEFAULT_REGULAR:
+        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_regular);
+        break;
+      case PLATFORM_DEFAULT_MEDIUM:
+        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_medium);
+        break;
+      case GOOGLE_SANS_MEDIUM:
+      case GOOGLE_SANS_REGULAR:
+        loadCustomTypeface(googleSansEnumToStringDef(commonTypeface), fontDetails, textView);
+        // The host should take care of italicization for custom fonts, so return here.
+        return;
+      default:
+        // Unrecognized common typeface. Try to load the next typeface from fontDetails.
+        // This should never happen.
+        fontDetails.currentTypefaceFailedToLoad();
+        loadFont(textView, fontDetails);
+        return;
+    }
+    makeFontItalic(textView, fontDetails.isItalic());
+  }
+
+  /** Ask the host to load a typeface by string identifier. */
+  private void loadCustomTypeface(
+      String customTypefaceName, FontDetails fontDetails, TextView textView) {
+    TypefaceCallback typefaceCallback = new TypefaceCallback(textView, fontDetails);
+    getParameters()
+        .hostProviders
+        .getAssetProvider()
+        .getTypeface(customTypefaceName, fontDetails.isItalic(), typefaceCallback);
+  }
+
+  /**
+   * Conversion method to avoid version skew issues if we would ever change the enum names in the
+   * CommonTypeface proto, so we don't need to change all the hosts or old clients.
+   */
+  @VisibleForTesting
+  @GoogleSansTypeface
+  static String googleSansEnumToStringDef(CommonTypeface googleSansType) {
+    switch (googleSansType) {
+      case GOOGLE_SANS_MEDIUM:
+        return GoogleSansTypeface.GOOGLE_SANS_MEDIUM;
+      case GOOGLE_SANS_REGULAR:
+        return GoogleSansTypeface.GOOGLE_SANS_REGULAR;
+      default:
+        return GoogleSansTypeface.UNDEFINED;
+    }
+  }
+
+  private static void makeFontItalic(TextView textView, boolean isItalic) {
+    if (isItalic) {
+      textView.setTypeface(textView.getTypeface(), Typeface.ITALIC);
+    } else {
+      textView.setTypeface(Typeface.create(textView.getTypeface(), Typeface.NORMAL));
+    }
+  }
+
+  private static TextView createView(Context context) {
+    TextView view = new TextView(context);
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+      view.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
+    }
+    return view;
+  }
+
+  TextElementKey createKey(Font font) {
+    return new TextElementKey(font);
+  }
+
+  abstract static class TextElementKeySupplier<A extends TextElementAdapter>
+      implements AdapterKeySupplier<A, TextElement> {
+    @Override
+    public TextElementKey getKey(FrameContext frameContext, TextElement model) {
+      StyleProvider styleProvider = frameContext.makeStyleFor(model.getStyleReferences());
+      return new TextElementKey(styleProvider.getFont());
+    }
+  }
+
+  /** We will Key TextViews off of Font Size, Typefaces and Italics. */
+  // LINT.IfChange
+  static class TextElementKey extends RecyclerKey {
+    private final int size;
+    private final boolean italic;
+    private final List<StylesProto.Typeface> typefaces;
+
+    TextElementKey(Font font) {
+      size = font.getSize();
+      italic = font.getItalic();
+      typefaces = font.getTypefaceList();
+    }
+
+    public int getSize() {
+      return size;
+    }
+
+    public boolean isItalic() {
+      return italic;
+    }
+
+    @Override
+    public int hashCode() {
+      // Can't use Objects.hash() as it is only available in KK+ and can't use Guava's impl either.
+      int result = size;
+      result = 31 * result + (italic ? 1 : 0);
+      result = 31 * result + typefaces.hashCode();
+      return result;
+    }
+
+    @Override
+    public boolean equals(/*@Nullable*/ Object obj) {
+      if (obj == this) {
+        return true;
+      }
+
+      if (obj == null) {
+        return false;
+      }
+
+      if (!(obj instanceof TextElementKey)) {
+        return false;
+      }
+
+      TextElementKey key = (TextElementKey) obj;
+      return key.size == size && key.italic == italic && typefaces.equals(key.typefaces);
+    }
+  }
+  // LINT.ThenChange
+
   static class ExtraLineHeight {
     private final int topPaddingPx;
     private final int bottomPaddingPx;
@@ -302,194 +476,58 @@
     }
   }
 
-  @VisibleForTesting
-  // LINT.IfChange
-  void setValuesUsedInRecyclerKey(TextElementKey fontKey, FrameContext frameContext) {
-    // TODO: Implement typefaces
-    TextView textView = getBaseView();
-    textView.setTextSize(fontKey.getSize());
+  static class FontDetails {
+    private int fontIndexToLoad;
+    private final List<StylesProto.Typeface> typefaceList;
+    private final boolean isItalic;
+    private final FrameContext frameContextForErrors;
 
-    for (StylesProto.Typeface typeface : fontKey.typefaces) {
-      switch (typeface.getTypefaceSpecifierCase()) {
-        case COMMON_TYPEFACE:
-          if (loadCommonTypeface(
-              typeface.getCommonTypeface(), textView, fontKey.isItalic(), frameContext)) {
-            return;
-          }
-          break;
-        case CUSTOM_TYPEFACE:
-          if (loadCustomTypeface(typeface.getCustomTypeface(), textView, fontKey.isItalic())) {
-            return;
-          }
-          break;
-        default:
-          // do nothing
+    FontDetails(
+        List<StylesProto.Typeface> typefaceList, boolean isItalic, FrameContext frameContext) {
+      this.typefaceList = typefaceList;
+      this.isItalic = isItalic;
+      this.frameContextForErrors = frameContext;
+    }
+
+    FrameContext getFrameContext() {
+      return frameContextForErrors;
+    }
+
+    StylesProto./*@Nullable*/ Typeface getTypefaceToLoad() {
+      if (typefaceList.size() <= fontIndexToLoad) {
+        return null;
       }
-    }
-    // We didn't load a font, but we can at least respect italicization.
-    makeFontItalic(textView, fontKey.isItalic());
-  }
-
-  /**
-   * Load one of the typefaces from the {@link CommonTypeface} enum.
-   *
-   * @return true for success, false for failure
-   */
-  private boolean loadCommonTypeface(
-      CommonTypeface commonTypeface,
-      TextView textView,
-      boolean isItalic,
-      FrameContext frameContext) {
-    switch (commonTypeface) {
-      case PLATFORM_DEFAULT_LIGHT:
-        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_light);
-        break;
-      case PLATFORM_DEFAULT_REGULAR:
-        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_regular);
-        break;
-      case PLATFORM_DEFAULT_MEDIUM:
-        TextViewCompat.setTextAppearance(textView, R.style.gm_font_weight_medium);
-        break;
-      case GOOGLE_SANS_MEDIUM:
-      case GOOGLE_SANS_REGULAR:
-        return loadGoogleSans(commonTypeface, textView, isItalic, frameContext);
-      default:
-        return false;
+      return typefaceList.get(fontIndexToLoad);
     }
 
-    makeFontItalic(textView, isItalic);
-    return true;
-  }
-
-  /**
-   * Ask the host to load a typeface by string identifier.
-   *
-   * @return true for success, false for failure
-   */
-  private boolean loadCustomTypeface(String customTypeface, TextView textView, boolean isItalic) {
-    Typeface hostTypeface =
-        getParameters().hostProviders.getAssetProvider().getTypeface(customTypeface, isItalic);
-    if (hostTypeface != null) {
-      textView.setTypeface(hostTypeface);
-      return true;
+    void currentTypefaceFailedToLoad() {
+      fontIndexToLoad++;
     }
-    return false;
-  }
 
-  /**
-   * Ask the host to load a Google Sans variant. These typefaces are expected to be present, but
-   * can't be included in the Piet library.
-   *
-   * @return true for success, false for failure
-   */
-  private boolean loadGoogleSans(
-      CommonTypeface googleSansType,
-      TextView textView,
-      boolean isItalic,
-      FrameContext frameContext) {
-    boolean success =
-        loadCustomTypeface(googleSansEnumToStringDef(googleSansType), textView, isItalic);
-    if (!success) {
-      frameContext.reportMessage(
-          MessageType.ERROR, ErrorCode.ERR_MISSING_FONTS, "Could not load Google Sans");
-    }
-    return success;
-  }
-  // LINT.ThenChange
-
-  /**
-   * Conversion method to avoid version skew issues if we would ever change the enum names in the
-   * CommonTypeface proto, so we don't need to change all the hosts or old clients.
-   */
-  @VisibleForTesting
-  @GoogleSansTypeface
-  static String googleSansEnumToStringDef(CommonTypeface googleSansType) {
-    switch (googleSansType) {
-      case GOOGLE_SANS_MEDIUM:
-        return GoogleSansTypeface.GOOGLE_SANS_MEDIUM;
-      case GOOGLE_SANS_REGULAR:
-        return GoogleSansTypeface.GOOGLE_SANS_REGULAR;
-      default:
-        return GoogleSansTypeface.UNDEFINED;
+    boolean isItalic() {
+      return isItalic;
     }
   }
 
-  private static void makeFontItalic(TextView textView, boolean isItalic) {
-    if (isItalic) {
-      textView.setTypeface(textView.getTypeface(), Typeface.ITALIC);
-    } else {
-      textView.setTypeface(Typeface.create(textView.getTypeface(), Typeface.NORMAL));
-    }
-  }
+  class TypefaceCallback implements Consumer</*@Nullable*/ Typeface> {
+    private final TextView textView;
+    private final FontDetails fontDetails;
 
-  private static TextView createView(Context context) {
-    TextView view = new TextView(context);
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
-      view.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
-    }
-    return view;
-  }
-
-  TextElementKey createKey(Font font) {
-    return new TextElementKey(font);
-  }
-
-  abstract static class TextElementKeySupplier<A extends TextElementAdapter>
-      implements AdapterKeySupplier<A, TextElement> {
-    @Override
-    public TextElementKey getKey(FrameContext frameContext, TextElement model) {
-      StyleProvider styleProvider = frameContext.makeStyleFor(model.getStyleReferences());
-      return new TextElementKey(styleProvider.getFont());
-    }
-  }
-
-  /** We will Key TextViews off of the Ellipsizing, Font Size and FontWeight, and Italics. */
-  // LINT.IfChange
-  static class TextElementKey extends RecyclerKey {
-    private final int size;
-    private final boolean italic;
-    private final List<StylesProto.Typeface> typefaces;
-
-    TextElementKey(Font font) {
-      size = font.getSize();
-      italic = font.getItalic();
-      typefaces = font.getTypefaceList();
-    }
-
-    public int getSize() {
-      return size;
-    }
-
-    public boolean isItalic() {
-      return italic;
+    TypefaceCallback(TextView textView, FontDetails fontDetails) {
+      this.textView = textView;
+      this.fontDetails = fontDetails;
     }
 
     @Override
-    public int hashCode() {
-      // Can't use Objects.hash() as it is only available in KK+ and can't use Guava's impl either.
-      int result = size;
-      result = 31 * result + (italic ? 1 : 0);
-      result = 31 * result + typefaces.hashCode();
-      return result;
-    }
-
-    @Override
-    public boolean equals(/*@Nullable*/ Object obj) {
-      if (obj == this) {
-        return true;
+    public void accept(/*@Nullable*/ Typeface typeface) {
+      if (typeface == null) {
+        fontDetails.currentTypefaceFailedToLoad();
+        loadFont(textView, fontDetails);
+        return;
       }
-
-      if (obj == null) {
-        return false;
+      if (!textView.getTypeface().equals(typeface)) {
+        textView.setTypeface(typeface);
       }
-
-      if (!(obj instanceof TextElementKey)) {
-        return false;
-      }
-
-      TextElementKey key = (TextElementKey) obj;
-      return key.size == size && key.italic == italic && typefaces.equals(key.typefaces);
     }
   }
-  // LINT.ThenChange
 }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/host/AssetProvider.java b/src/main/java/com/google/android/libraries/feed/piet/host/AssetProvider.java
index ddea907..ce68532 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/host/AssetProvider.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/host/AssetProvider.java
@@ -103,9 +103,9 @@
    * the {@link GoogleSansTypeface} StringDef. Piet will report errors if Google Sans is requested
    * and not found.
    */
-  /*@Nullable*/
-  public Typeface getTypeface(String typeface, boolean isItalic) {
-    return typefaceProvider.getTypeface(typeface, isItalic);
+  public void getTypeface(
+      String typeface, boolean isItalic, Consumer</*@Nullable*/ Typeface> consumer) {
+    typefaceProvider.getTypeface(typeface, isItalic, consumer);
   }
 
   /** Returns whether Piet should render layouts using a right-to-left orientation. */
diff --git a/src/main/java/com/google/android/libraries/feed/piet/host/NullTypefaceProvider.java b/src/main/java/com/google/android/libraries/feed/piet/host/NullTypefaceProvider.java
index 3994945..89f9b0b 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/host/NullTypefaceProvider.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/host/NullTypefaceProvider.java
@@ -15,13 +15,14 @@
 package com.google.android.libraries.feed.piet.host;
 
 import android.graphics.Typeface;
+import com.google.android.libraries.feed.common.functional.Consumer;
 
 /** Typeface provider that does not provide any typefaces; for use as a default implementation. */
 public class NullTypefaceProvider implements TypefaceProvider {
 
   @Override
-  /*@Nullable*/
-  public Typeface getTypeface(String typeface, boolean isItalic) {
-    return null;
+  public void getTypeface(
+      String typeface, boolean isItalic, Consumer</*@Nullable*/ Typeface> consumer) {
+    consumer.accept(null);
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/host/TypefaceProvider.java b/src/main/java/com/google/android/libraries/feed/piet/host/TypefaceProvider.java
index 2d10390..d93c34c 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/host/TypefaceProvider.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/host/TypefaceProvider.java
@@ -16,21 +16,32 @@
 
 import android.graphics.Typeface;
 import android.support.annotation.StringDef;
+import com.google.android.libraries.feed.common.functional.Consumer;
 
 /** Allows the host to provide Typefaces to Piet. */
 public interface TypefaceProvider {
   /**
-   * Allows the host to return a typeface Piet would otherwise not be able to access (ex. from
-   * assets). Piet will call this when the typeface is not one Piet recognizes (as a default Android
-   * typeface). If host does not specially handle the specified typeface, host can return {@code
-   * null}, and Piet will proceed through its fallback typefaces.
+   * Allows the host to load a typeface Piet would otherwise not be able to access (ex. from
+   * assets), and return it via the consumer. Piet will call this when the typeface is not one Piet
+   * recognizes (as a default Android typeface). If host does not specially handle the specified
+   * typeface, host can accept {@code null} through the consumer, and Piet will proceed through its
+   * fallback typefaces.
    *
    * <p>Piet also expects the host to provide the Google Sans typeface, and will request it using
    * the {@link GoogleSansTypeface} StringDef. Piet will report errors if Google Sans is requested
    * and not found.
+   *
+   * @param typeface the String that the host uses to identify which typeface to load.
+   * @param isItalic specifies whether the font should be italic. This is passed to the host instead
+   *     of being handled by Piet so that the host can decide whether to just set the italic bits
+   *     for the typeface or, if they want, return an entirely different font for the italic
+   *     version.
+   * @param consumer accepts the typeface once it's loaded, via consumer.accept(Typeface). If the
+   *     host does not recognize the typeface name or fails to load the typeface, it should accept
+   *     {@code null}. If the host does NOT call consumer.accept(null), Piet will not load the
+   *     fallback font, and will just use the platform default.
    */
-  /*@Nullable*/
-  Typeface getTypeface(String typeface, boolean isItalic);
+  void getTypeface(String typeface, boolean isItalic, Consumer</*@Nullable*/ Typeface> consumer);
 
   /**
    * Strings the host can expect to receive to request Google Sans fonts. These are intentionally
diff --git a/src/test/java/com/google/android/libraries/feed/piet/BUILD b/src/test/java/com/google/android/libraries/feed/piet/BUILD
index b1d446f..0cd5cc3 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/piet/BUILD
@@ -531,6 +531,7 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
+        "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
         "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/common/ui",
diff --git a/src/test/java/com/google/android/libraries/feed/piet/PietManagerImplTest.java b/src/test/java/com/google/android/libraries/feed/piet/PietManagerImplTest.java
index 0d6f1af..2a07f2b 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/PietManagerImplTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/PietManagerImplTest.java
@@ -21,6 +21,7 @@
 
 import android.app.Activity;
 import android.content.Context;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -206,8 +207,9 @@
     assetProvider.getRelativeElapsedString(789);
     verify(stringFormatter).getRelativeElapsedString(789);
 
-    assetProvider.getTypeface("blah", true);
-    verify(typefaceProvider).getTypeface("blah", true);
+    Consumer<Typeface> typefaceConsumer = typeface -> {};
+    assetProvider.getTypeface("blah", false, typefaceConsumer);
+    verify(typefaceProvider).getTypeface("blah", false, typefaceConsumer);
 
     assertThat(assetProvider.isDarkTheme()).isEqualTo(isDarkTheme);
     assertThat(assetProvider.isRtL()).isEqualTo(isRtL);
diff --git a/src/test/java/com/google/android/libraries/feed/piet/TextElementAdapterTest.java b/src/test/java/com/google/android/libraries/feed/piet/TextElementAdapterTest.java
index 1de4a54..2719f51 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/TextElementAdapterTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/TextElementAdapterTest.java
@@ -19,6 +19,10 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -34,11 +38,13 @@
 import android.view.Gravity;
 import android.view.View;
 import android.widget.TextView;
+import com.google.android.libraries.feed.common.functional.Consumer;
 import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.piet.DebugLogger.MessageType;
 import com.google.android.libraries.feed.piet.TextElementAdapter.TextElementKey;
 import com.google.android.libraries.feed.piet.host.AssetProvider;
+import com.google.android.libraries.feed.piet.host.TypefaceProvider;
 import com.google.android.libraries.feed.piet.host.TypefaceProvider.GoogleSansTypeface;
 import com.google.search.now.ui.piet.BindingRefsProto.StyleBindingRef;
 import com.google.search.now.ui.piet.ElementsProto.CustomElement;
@@ -56,6 +62,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
@@ -68,6 +76,7 @@
   @Mock private StyleProvider mockStyleProvider;
   @Mock private HostProviders mockHostProviders;
   @Mock private AssetProvider mockAssetProvider;
+  @Mock private TypefaceProvider mockTypefaceProvider;
 
   private AdapterParameters adapterParameters;
 
@@ -142,7 +151,8 @@
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
 
-    verify(mockAssetProvider, never()).getTypeface(any(), anyBoolean());
+    verify(mockAssetProvider, never())
+        .getTypeface(anyString(), anyBoolean(), ArgumentMatchers.<Consumer<Typeface>>any());
   }
 
   @Test
@@ -151,15 +161,12 @@
         Font.newBuilder()
             .addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
             .build();
-    Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
-    when(mockAssetProvider.getTypeface("goodfont", false)).thenReturn(hostTypeface);
-
     TextElementKey key = new TextElementKey(font);
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
-    Typeface typeface = adapter.getBaseView().getTypeface();
 
-    assertThat(typeface).isEqualTo(hostTypeface);
+    verify(mockAssetProvider, atLeastOnce())
+        .getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
   }
 
   @Test
@@ -170,15 +177,12 @@
             .addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("badfont"))
             .setItalic(true)
             .build();
-    Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
-    when(mockAssetProvider.getTypeface("goodfont", true)).thenReturn(hostTypeface);
-
     TextElementKey key = new TextElementKey(font);
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
-    Typeface typeface = adapter.getBaseView().getTypeface();
 
-    assertThat(typeface).isEqualTo(hostTypeface);
+    verify(mockAssetProvider, atLeastOnce())
+        .getTypeface(eq("goodfont"), eq(true), ArgumentMatchers.<Consumer<Typeface>>any());
   }
 
   @Test
@@ -188,15 +192,38 @@
             .addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("badfont"))
             .addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
             .build();
-    Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
-    when(mockAssetProvider.getTypeface("goodfont", false)).thenReturn(hostTypeface);
-
     TextElementKey key = new TextElementKey(font);
+    // Consumer accepts null for badfont
+    doAnswer(
+            answer -> {
+              Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
+              typefaceConsumer.accept(null);
+              return null;
+            })
+        .when(mockAssetProvider)
+        .getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
+    // Consumer accepts hosttypeface for goodfont
+    Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
+    doAnswer(
+            answer -> {
+              Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
+              typefaceConsumer.accept(hostTypeface);
+              return null;
+            })
+        .when(mockAssetProvider)
+        .getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
-    Typeface typeface = adapter.getBaseView().getTypeface();
 
+    Typeface typeface = adapter.getBaseView().getTypeface();
     assertThat(typeface).isEqualTo(hostTypeface);
+    InOrder inOrder = inOrder(mockAssetProvider);
+    inOrder
+        .verify(mockAssetProvider, atLeastOnce())
+        .getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
+    inOrder
+        .verify(mockAssetProvider, atLeastOnce())
+        .getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
   }
 
   @Test
@@ -205,13 +232,24 @@
         Font.newBuilder()
             .addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("notvalid"))
             .build();
-    when(mockAssetProvider.getTypeface(anyString(), anyBoolean())).thenReturn(null);
-
+    doAnswer(
+            answer -> {
+              Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
+              typefaceConsumer.accept(null);
+              return null;
+            })
+        .when(mockAssetProvider)
+        .getTypeface(eq("notvalid"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
     TextElementKey key = new TextElementKey(font);
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
     Typeface typeface = adapter.getBaseView().getTypeface();
 
+    verify(frameContext)
+        .reportMessage(
+            MessageType.WARNING,
+            ErrorCode.ERR_MISSING_FONTS,
+            "Could not load specified typefaces.");
     assertThat(typeface).isEqualTo(new TextView(context).getTypeface());
   }
 
@@ -223,36 +261,15 @@
                 StylesProto.Typeface.newBuilder()
                     .setCommonTypeface(CommonTypeface.GOOGLE_SANS_MEDIUM))
             .build();
-    Typeface hostTypeface = Typeface.create("fakegooglesans", Typeface.BOLD_ITALIC);
-    when(mockAssetProvider.getTypeface(GoogleSansTypeface.GOOGLE_SANS_MEDIUM, false))
-        .thenReturn(hostTypeface);
-
-    TextElementKey key = new TextElementKey(font);
-
-    adapter.setValuesUsedInRecyclerKey(key, frameContext);
-    Typeface typeface = adapter.getBaseView().getTypeface();
-
-    assertThat(typeface).isEqualTo(hostTypeface);
-  }
-
-  @Test
-  public void testSetFont_reportsFailureWithGoogleSans() {
-    Font font =
-        Font.newBuilder()
-            .addTypeface(
-                StylesProto.Typeface.newBuilder()
-                    .setCommonTypeface(CommonTypeface.GOOGLE_SANS_MEDIUM))
-            .build();
-    when(mockAssetProvider.getTypeface(GoogleSansTypeface.GOOGLE_SANS_MEDIUM, false))
-        .thenReturn(null);
-
     TextElementKey key = new TextElementKey(font);
 
     adapter.setValuesUsedInRecyclerKey(key, frameContext);
 
-    verify(frameContext)
-        .reportMessage(
-            MessageType.ERROR, ErrorCode.ERR_MISSING_FONTS, "Could not load Google Sans");
+    verify(mockAssetProvider, atLeastOnce())
+        .getTypeface(
+            eq(GoogleSansTypeface.GOOGLE_SANS_MEDIUM),
+            eq(false),
+            ArgumentMatchers.<Consumer<Typeface>>any());
   }
 
   @Test
@@ -684,9 +701,7 @@
     }
 
     @Override
-    void setTextOnView(FrameContext frameContext, TextElement textElement) {
-      return;
-    }
+    void setTextOnView(FrameContext frameContext, TextElement textElement) {}
 
     @Override
     TextElementKey createKey(Font font) {
diff --git a/src/test/java/com/google/android/libraries/feed/piet/host/AssetProviderTest.java b/src/test/java/com/google/android/libraries/feed/piet/host/AssetProviderTest.java
index 96e03f6..e295f44 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/host/AssetProviderTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/host/AssetProviderTest.java
@@ -83,8 +83,14 @@
   @Test
   public void testNullTypefaceProvider() {
     TypefaceProvider typefaceProvider = new NullTypefaceProvider();
+    final Object[] consumedObject = {""};
 
-    assertThat(typefaceProvider.getTypeface("GOOGLE_SANS_MEDIUM", false)).isNull();
+    // Make sure the object isn't already null, or we'll get a false positive.
+    assertThat(consumedObject[0]).isNotNull();
+    // The consumer passed in just saves the value that is consumed, so we can check that it's null.
+    typefaceProvider.getTypeface(
+        "GOOGLE_SANS_MEDIUM", false, typeface -> consumedObject[0] = typeface);
+    assertThat(consumedObject[0]).isNull();
   }
 
   @Test