1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.N_MR1; 4 import static android.os.Build.VERSION_CODES.O; 5 import static android.os.Build.VERSION_CODES.O_MR1; 6 import static android.os.Build.VERSION_CODES.P; 7 import static android.os.Build.VERSION_CODES.Q; 8 import static android.os.Build.VERSION_CODES.R; 9 import static android.os.Build.VERSION_CODES.S; 10 import static org.robolectric.Shadows.shadowOf; 11 import static org.robolectric.util.reflector.Reflector.reflector; 12 13 import android.annotation.SuppressLint; 14 import android.content.res.AssetManager; 15 import android.graphics.Typeface; 16 import android.graphics.fonts.FontStyle; 17 import android.util.ArrayMap; 18 import java.io.File; 19 import java.io.IOException; 20 import java.nio.file.Files; 21 import java.nio.file.Path; 22 import java.util.Collection; 23 import java.util.Map; 24 import java.util.Objects; 25 import java.util.concurrent.atomic.AtomicLong; 26 import org.robolectric.RuntimeEnvironment; 27 import org.robolectric.annotation.ClassName; 28 import org.robolectric.annotation.HiddenApi; 29 import org.robolectric.annotation.Implementation; 30 import org.robolectric.annotation.Implements; 31 import org.robolectric.annotation.RealObject; 32 import org.robolectric.res.Fs; 33 import org.robolectric.shadow.api.Shadow; 34 import org.robolectric.util.ReflectionHelpers; 35 import org.robolectric.util.ReflectionHelpers.ClassParameter; 36 import org.robolectric.util.reflector.Accessor; 37 import org.robolectric.util.reflector.Direct; 38 import org.robolectric.util.reflector.ForType; 39 40 /** Shadow for {@link Typeface}. */ 41 @Implements(value = Typeface.class, isInAndroidSdk = false) 42 @SuppressLint("NewApi") 43 public class ShadowLegacyTypeface extends ShadowTypeface { 44 private static final AtomicLong nextFontId = new AtomicLong(1); 45 private FontDesc description; 46 47 @RealObject Typeface realTypeface; 48 49 @Implementation __staticInitializer__()50 protected static void __staticInitializer__() { 51 Shadow.directInitialize(Typeface.class); 52 if (RuntimeEnvironment.getApiLevel() > R) { 53 Typeface.loadPreinstalledSystemFontMap(); 54 } 55 } 56 57 @Implementation(minSdk = P) create(Typeface family, int weight, boolean italic)58 protected static Typeface create(Typeface family, int weight, boolean italic) { 59 if (family == null) { 60 return createUnderlyingTypeface(null, weight); 61 } else { 62 ShadowTypeface shadowTypeface = Shadow.extract(family); 63 return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), weight); 64 } 65 } 66 67 @Implementation create(String familyName, int style)68 protected static Typeface create(String familyName, int style) { 69 return createUnderlyingTypeface(familyName, style); 70 } 71 72 @Implementation create(Typeface family, int style)73 protected static Typeface create(Typeface family, int style) { 74 if (family == null) { 75 return createUnderlyingTypeface(null, style); 76 } else { 77 ShadowTypeface shadowTypeface = Shadow.extract(family); 78 return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style); 79 } 80 } 81 82 @Implementation createFromAsset(AssetManager mgr, String path)83 protected static Typeface createFromAsset(AssetManager mgr, String path) { 84 ShadowAssetManager shadowAssetManager = Shadow.extract(mgr); 85 Collection<Path> assetDirs = shadowAssetManager.getAllAssetDirs(); 86 for (Path assetDir : assetDirs) { 87 Path assetFile = assetDir.resolve(path); 88 if (Files.exists(assetFile)) { 89 return createUnderlyingTypeface(path, Typeface.NORMAL); 90 } 91 92 // maybe path is e.g. "myFont", but we should match "myFont.ttf" too? 93 Path[] files; 94 try { 95 files = Fs.listFiles(assetDir, f -> f.getFileName().toString().startsWith(path)); 96 } catch (IOException e) { 97 throw new RuntimeException(e); 98 } 99 if (files.length != 0) { 100 return createUnderlyingTypeface(path, Typeface.NORMAL); 101 } 102 } 103 104 throw new RuntimeException("Font asset not found " + path); 105 } 106 107 @Implementation(minSdk = O, maxSdk = P) createFromResources(AssetManager mgr, String path, int cookie)108 protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) { 109 return createUnderlyingTypeface(path, Typeface.NORMAL); 110 } 111 112 @Implementation(minSdk = O) createFromResources( @lassName"android.content.res.FontResourcesParser$FamilyResourceEntry") Object entry, AssetManager mgr, String path)113 protected static Typeface createFromResources( 114 @ClassName("android.content.res.FontResourcesParser$FamilyResourceEntry") Object entry, 115 AssetManager mgr, 116 String path) { 117 return createUnderlyingTypeface(path, Typeface.NORMAL); 118 } 119 120 @Implementation createFromFile(File path)121 protected static Typeface createFromFile(File path) { 122 String familyName = path.toPath().getFileName().toString(); 123 return createUnderlyingTypeface(familyName, Typeface.NORMAL); 124 } 125 126 @Implementation createFromFile(String path)127 protected static Typeface createFromFile(String path) { 128 return createFromFile(new File(path)); 129 } 130 131 @Implementation getStyle()132 protected int getStyle() { 133 return description.getStyle(); 134 } 135 136 @Override 137 @Implementation equals(Object o)138 public boolean equals(Object o) { 139 if (o instanceof Typeface) { 140 Typeface other = ((Typeface) o); 141 return Objects.equals(getFontDescription(), shadowOf(other).getFontDescription()); 142 } 143 return false; 144 } 145 146 @Override 147 @Implementation hashCode()148 public int hashCode() { 149 return getFontDescription().hashCode(); 150 } 151 152 @HiddenApi 153 @Implementation createFromFamilies( @lassName"[Landroid.graphics.FontFamily;") Object families)154 protected static Typeface createFromFamilies( 155 @ClassName("[Landroid.graphics.FontFamily;") Object families) { 156 return null; 157 } 158 159 @HiddenApi 160 @Implementation(maxSdk = N_MR1) createFromFamiliesWithDefault( @lassName"[Landroid.graphics.FontFamily;") Object families)161 protected static Typeface createFromFamiliesWithDefault( 162 @ClassName("[Landroid.graphics.FontFamily;") Object families) { 163 return null; 164 } 165 166 @Implementation(minSdk = O, maxSdk = O_MR1) createFromFamiliesWithDefault( @lassName"[Landroid.graphics.FontFamily;") Object families, int weight, int italic)167 protected static Typeface createFromFamiliesWithDefault( 168 @ClassName("[Landroid.graphics.FontFamily;") Object families, int weight, int italic) { 169 return createUnderlyingTypeface("fake-font", Typeface.NORMAL); 170 } 171 172 @Implementation(minSdk = P) createFromFamiliesWithDefault( @lassName"[Landroid.graphics.FontFamily;") Object families, String fallbackName, int weight, int italic)173 protected static Typeface createFromFamiliesWithDefault( 174 @ClassName("[Landroid.graphics.FontFamily;") Object families, 175 String fallbackName, 176 int weight, 177 int italic) { 178 return createUnderlyingTypeface(fallbackName, Typeface.NORMAL); 179 } 180 181 @Implementation(minSdk = P, maxSdk = P) buildSystemFallback( String xmlPath, String fontDir, ArrayMap<String, Typeface> fontMap, ArrayMap<String, ?> fallbackMap)182 protected static void buildSystemFallback( 183 String xmlPath, 184 String fontDir, 185 ArrayMap<String, Typeface> fontMap, 186 ArrayMap<String, /*android.graphics.FontFamily[]*/ ?> fallbackMap) { 187 fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0)); 188 } 189 190 /** Avoid spurious error message about /system/etc/fonts.xml */ 191 @Implementation(maxSdk = O_MR1) init()192 protected static void init() {} 193 194 @HiddenApi 195 @Implementation(minSdk = Q, maxSdk = R) initSystemDefaultTypefaces( Map<String, Typeface> systemFontMap, Map<String, ?> fallbacks, @ClassName("[Landroid.text.FontConfig$Alias;") Object aliases)196 protected static void initSystemDefaultTypefaces( 197 Map<String, Typeface> systemFontMap, 198 Map<String, /*android.graphics.FontFamily[]*/ ?> fallbacks, 199 @ClassName("[Landroid.text.FontConfig$Alias;") Object aliases) {} 200 createUnderlyingTypeface(String familyName, int style)201 protected static Typeface createUnderlyingTypeface(String familyName, int style) { 202 long thisFontId = nextFontId.getAndIncrement(); 203 Typeface result = 204 ReflectionHelpers.callConstructor( 205 Typeface.class, ClassParameter.from(long.class, thisFontId)); 206 ((ShadowLegacyTypeface) Shadow.extract(result)).description = new FontDesc(familyName, style); 207 return result; 208 } 209 210 211 @Implementation(minSdk = O, maxSdk = R) nativeCreateFromArray(long[] familyArray, int weight, int italic)212 protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) { 213 // TODO: implement this properly 214 return nextFontId.incrementAndGet(); 215 } 216 217 /** 218 * Returns the font description. 219 * 220 * @return Font description. 221 */ 222 @Override getFontDescription()223 public FontDesc getFontDescription() { 224 return description; 225 } 226 227 @Implementation(minSdk = S) nativeForceSetStaticFinalField(String fieldname, Typeface typeface)228 protected static void nativeForceSetStaticFinalField(String fieldname, Typeface typeface) { 229 ReflectionHelpers.setStaticField(Typeface.class, fieldname, typeface); 230 } 231 232 @Implementation(minSdk = S) nativeCreateFromArray( long[] familyArray, long fallbackTypeface, int weight, int italic)233 protected static long nativeCreateFromArray( 234 long[] familyArray, long fallbackTypeface, int weight, int italic) { 235 return ShadowLegacyTypeface.nativeCreateFromArray(familyArray, weight, italic); 236 } 237 238 /** Shadow for {@link Typeface.Builder} */ 239 @Implements(value = Typeface.Builder.class, minSdk = Q) 240 public static class ShadowBuilder { 241 @RealObject Typeface.Builder realBuilder; 242 243 @Implementation build()244 protected Typeface build() { 245 String path = ReflectionHelpers.getField(realBuilder, "mPath"); 246 return createUnderlyingTypeface(path, Typeface.NORMAL); 247 } 248 } 249 250 /** Shadow for {@link Typeface.CustomFallbackBuilder} that populates {@link #description} */ 251 @Implements( 252 value = Typeface.CustomFallbackBuilder.class, 253 minSdk = Q, 254 shadowPicker = CustomFallbackBuilderPicker.class) 255 public static class ShadowCustomFallbackBuilder { 256 @RealObject Typeface.CustomFallbackBuilder realBuilder; 257 258 @Implementation build()259 protected Typeface build() { 260 Typeface result = reflector(CustomFallbackBuilderReflector.class, realBuilder).build(); 261 FontStyle style = reflector(CustomFallbackBuilderReflector.class, realBuilder).getStyle(); 262 ((ShadowLegacyTypeface) Shadow.extract(result)).description = 263 new FontDesc(null, style.getWeight()); 264 return result; 265 } 266 } 267 268 /** Shadow picker for {@link Typeface.CustomFallbackBuilder}. */ 269 public static final class CustomFallbackBuilderPicker extends GraphicsShadowPicker<Object> { CustomFallbackBuilderPicker()270 public CustomFallbackBuilderPicker() { 271 super(ShadowLegacyTypeface.ShadowCustomFallbackBuilder.class, null); 272 } 273 } 274 275 @ForType(Typeface.CustomFallbackBuilder.class) 276 interface CustomFallbackBuilderReflector { 277 @Direct build()278 Typeface build(); 279 280 @Accessor("mStyle") getStyle()281 FontStyle getStyle(); 282 } 283 } 284