Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.LOLLIPOP;
      4 import static android.os.Build.VERSION_CODES.N_MR1;
      5 import static android.os.Build.VERSION_CODES.O;
      6 import static android.os.Build.VERSION_CODES.O_MR1;
      7 import static android.os.Build.VERSION_CODES.P;
      8 import static org.robolectric.RuntimeEnvironment.getApiLevel;
      9 
     10 import android.annotation.SuppressLint;
     11 import android.content.res.AssetManager;
     12 import android.graphics.FontFamily;
     13 import android.graphics.Typeface;
     14 import android.util.ArrayMap;
     15 import java.io.File;
     16 import java.util.Collection;
     17 import java.util.HashMap;
     18 import java.util.Map;
     19 import org.robolectric.annotation.HiddenApi;
     20 import org.robolectric.annotation.Implementation;
     21 import org.robolectric.annotation.Implements;
     22 import org.robolectric.annotation.RealObject;
     23 import org.robolectric.annotation.Resetter;
     24 import org.robolectric.res.FsFile;
     25 import org.robolectric.shadow.api.Shadow;
     26 import org.robolectric.util.ReflectionHelpers;
     27 import org.robolectric.util.ReflectionHelpers.ClassParameter;
     28 
     29 @Implements(value = Typeface.class, looseSignatures = true)
     30 @SuppressLint("NewApi")
     31 public class ShadowTypeface {
     32   private static Map<Long, FontDesc> FONTS = new HashMap<>();
     33   private static long nextFontId = 1;
     34   private FontDesc description;
     35   @RealObject private Typeface realTypeface;
     36 
     37   @HiddenApi
     38   @Implementation
     39   protected void __constructor__(int fontId) {
     40     description = findById((long) fontId);
     41   }
     42 
     43   @HiddenApi
     44   @Implementation
     45   protected void __constructor__(long fontId) {
     46     description = findById(fontId);
     47   }
     48 
     49   @Implementation
     50   protected static Typeface create(String familyName, int style) {
     51     return createUnderlyingTypeface(familyName, style);
     52   }
     53 
     54   @Implementation
     55   protected static Typeface create(Typeface family, int style) {
     56     if (family == null) {
     57       return createUnderlyingTypeface(null, style);
     58     } else {
     59       ShadowTypeface shadowTypeface = Shadow.extract(family);
     60       return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style);
     61     }
     62   }
     63 
     64   @Implementation
     65   protected static Typeface createFromAsset(AssetManager mgr, String path) {
     66     ShadowAssetManager shadowAssetManager = Shadow.extract(mgr);
     67     Collection<FsFile> assetDirs = shadowAssetManager.getAllAssetDirs();
     68     for (FsFile assetDir : assetDirs) {
     69       FsFile[] files = assetDir.listFiles(new StartsWith(path));
     70       FsFile assetFile = assetDir.join(path);
     71       if (assetFile.exists() || files.length != 0) {
     72         return createUnderlyingTypeface(path, Typeface.NORMAL);
     73       }
     74     }
     75 
     76     throw new RuntimeException("Font asset not found " + path);
     77   }
     78 
     79   @Implementation(minSdk = O)
     80   protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
     81     return createUnderlyingTypeface(path, Typeface.NORMAL);
     82   }
     83 
     84   @Implementation
     85   protected static Typeface createFromFile(File path) {
     86     String familyName = path.toPath().getFileName().toString();
     87     return createUnderlyingTypeface(familyName, Typeface.NORMAL);
     88   }
     89 
     90   @Implementation
     91   protected static Typeface createFromFile(String path) {
     92     return createFromFile(new File(path));
     93   }
     94 
     95   @Implementation
     96   protected int getStyle() {
     97     return description.getStyle();
     98   }
     99 
    100   @HiddenApi
    101   @Implementation(minSdk = LOLLIPOP)
    102   protected static Typeface createFromFamilies(Object /*FontFamily[]*/ families) {
    103     return null;
    104   }
    105 
    106   @HiddenApi
    107   @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
    108   protected static Typeface createFromFamiliesWithDefault(Object /*FontFamily[]*/ families) {
    109     return null;
    110   }
    111 
    112   @Implementation(minSdk = O, maxSdk = O_MR1)
    113   protected static Typeface createFromFamiliesWithDefault(
    114       Object /*FontFamily[]*/ families, Object /* int */ weight, Object /* int */ italic) {
    115     return createUnderlyingTypeface("fake-font", Typeface.NORMAL);
    116   }
    117 
    118   @Implementation(minSdk = P)
    119   protected static Typeface createFromFamiliesWithDefault(
    120       Object /*FontFamily[]*/ families,
    121       Object /* String */ fallbackName,
    122       Object /* int */ weight,
    123       Object /* int */ italic) {
    124     return createUnderlyingTypeface((String) fallbackName, Typeface.NORMAL);
    125   }
    126 
    127   @Implementation(minSdk = P)
    128   protected static void buildSystemFallback(String xmlPath, String fontDir,
    129       ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap) {
    130     fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0));
    131   }
    132 
    133   // BEGIN-INTERNAL
    134   @HiddenApi
    135   @Implementation(minSdk = android.os.Build.VERSION_CODES.Q)
    136   public static void initSystemDefaultTypefaces(Object systemFontMap,
    137       Object fallbacks,
    138       Object aliases) {
    139   }
    140   // END-INTERNAL
    141 
    142   @Resetter
    143   synchronized public static void reset() {
    144     FONTS.clear();
    145   }
    146 
    147   private static Typeface createUnderlyingTypeface(String familyName, int style) {
    148     long thisFontId = nextFontId++;
    149     FONTS.put(thisFontId, new FontDesc(familyName, style));
    150     if (getApiLevel() >= LOLLIPOP) {
    151       return ReflectionHelpers.callConstructor(Typeface.class, ClassParameter.from(long.class, thisFontId));
    152     } else {
    153       return ReflectionHelpers.callConstructor(Typeface.class, ClassParameter.from(int.class, (int) thisFontId));
    154     }
    155   }
    156 
    157   private synchronized static FontDesc findById(long fontId) {
    158     if (FONTS.containsKey(fontId)) {
    159       return FONTS.get(fontId);
    160     }
    161     throw new RuntimeException("Unknown font id: " + fontId);
    162   }
    163 
    164   /**
    165    * Returns the font description.
    166    *
    167    * @return Font description.
    168    */
    169   public FontDesc getFontDescription() {
    170     return description;
    171   }
    172 
    173   public static class FontDesc {
    174     public final String familyName;
    175     public final int style;
    176 
    177     public FontDesc(String familyName, int style) {
    178       this.familyName = familyName;
    179       this.style = style;
    180     }
    181 
    182     @Override
    183     public boolean equals(Object o) {
    184       if (this == o) return true;
    185       if (o == null || getClass() != o.getClass()) return false;
    186 
    187       FontDesc fontDesc = (FontDesc) o;
    188 
    189       if (style != fontDesc.style) return false;
    190       if (familyName != null ? !familyName.equals(fontDesc.familyName) : fontDesc.familyName != null)
    191         return false;
    192 
    193       return true;
    194     }
    195 
    196     @Override
    197     public int hashCode() {
    198       int result = familyName != null ? familyName.hashCode() : 0;
    199       result = 31 * result + style;
    200       return result;
    201     }
    202 
    203     public String getFamilyName() {
    204       return familyName;
    205     }
    206 
    207     public int getStyle() {
    208       return style;
    209     }
    210   }
    211 
    212   private static class StartsWith implements FsFile.Filter {
    213     private final String contains;
    214 
    215     public StartsWith(String contains) {
    216       this.contains = contains;
    217     }
    218 
    219     @Override
    220     public boolean accept(FsFile file) {
    221       return file.getName().startsWith(contains);
    222     }
    223   }
    224 }
    225