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