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