1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static android.os.Build.VERSION_CODES.O_MR1; 5 import static android.os.Build.VERSION_CODES.P; 6 import static android.os.Build.VERSION_CODES.Q; 7 import static android.os.Build.VERSION_CODES.R; 8 import static android.os.Build.VERSION_CODES.S; 9 import static android.os.Build.VERSION_CODES.TIRAMISU; 10 import static org.robolectric.util.reflector.Reflector.reflector; 11 12 import android.graphics.FontFamily; 13 import android.graphics.Typeface; 14 import android.graphics.fonts.FontVariationAxis; 15 import android.text.FontConfig; 16 import android.util.ArrayMap; 17 import android.util.Log; 18 import com.google.common.base.Preconditions; 19 import com.google.errorprone.annotations.CanIgnoreReturnValue; 20 import java.io.File; 21 import java.io.FileInputStream; 22 import java.io.IOException; 23 import java.nio.ByteBuffer; 24 import java.nio.channels.FileChannel; 25 import java.util.List; 26 import java.util.Map; 27 import org.robolectric.RuntimeEnvironment; 28 import org.robolectric.annotation.ClassName; 29 import org.robolectric.annotation.Implementation; 30 import org.robolectric.annotation.Implements; 31 import org.robolectric.annotation.InDevelopment; 32 import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader; 33 import org.robolectric.nativeruntime.TypefaceNatives; 34 import org.robolectric.shadow.api.Shadow; 35 import org.robolectric.util.reflector.Direct; 36 import org.robolectric.util.reflector.ForType; 37 import org.robolectric.util.reflector.Static; 38 import org.robolectric.versioning.AndroidVersions; 39 import org.robolectric.versioning.AndroidVersions.U; 40 41 /** Shadow for {@link Typeface} that is backed by native code */ 42 @Implements( 43 value = Typeface.class, 44 minSdk = O, 45 isInAndroidSdk = false, 46 callNativeMethodsByDefault = true) 47 public class ShadowNativeTypeface extends ShadowTypeface { 48 49 private static final String TAG = "ShadowNativeTypeface"; 50 51 // Style value for building typeface. 52 private static final int STYLE_NORMAL = 0; 53 private static final int STYLE_ITALIC = 1; 54 55 @Implementation(minSdk = S) __staticInitializer__()56 protected static void __staticInitializer__() { 57 if (RuntimeEnvironment.getApiLevel() <= U.SDK_INT) { 58 Shadow.directInitialize(Typeface.class); 59 // Initialize the system font map. In real Android this is done as part of Application startup 60 // and uses a more complex SharedMemory system not supported in Robolectric. 61 Typeface.loadPreinstalledSystemFontMap(); 62 } 63 // The Typeface static initializer invokes its own native methods. This has to be deferred 64 // starting in Android V. 65 } 66 67 @Implementation(minSdk = P, maxSdk = P) buildSystemFallback( String xmlPath, String systemFontDir, ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap)68 protected static void buildSystemFallback( 69 String xmlPath, 70 String systemFontDir, 71 ArrayMap<String, Typeface> fontMap, 72 ArrayMap<String, FontFamily[]> fallbackMap) { 73 String fontDir = System.getProperty("robolectric.nativeruntime.fontdir"); 74 Preconditions.checkNotNull(fontDir); 75 Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory"); 76 Preconditions.checkState( 77 fontDir.endsWith(File.separator), "Fonts directory must end with a slash"); 78 reflector(TypefaceReflector.class) 79 .buildSystemFallback(fontDir + "fonts.xml", fontDir, fontMap, fallbackMap); 80 } 81 82 @Implementation(minSdk = O, maxSdk = O_MR1) getSystemFontConfigLocation()83 protected static File getSystemFontConfigLocation() { 84 // Ensure that the Robolectric native runtime is loaded in ordere to ensure that the 85 // `robolectric.nativeruntime.fontdir` system property is valid. 86 DefaultNativeRuntimeLoader.injectAndLoad(); 87 String fontDir = System.getProperty("robolectric.nativeruntime.fontdir"); 88 Preconditions.checkNotNull(fontDir); 89 Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory"); 90 Preconditions.checkState( 91 fontDir.endsWith(File.separator), "Fonts directory must end with a slash"); 92 return new File(fontDir); 93 } 94 95 @SuppressWarnings("unchecked") 96 @Implementation(minSdk = O, maxSdk = O_MR1) makeFamilyFromParsed( @lassName"android.text.FontConfig$Family") Object family, Map<String, ByteBuffer> bufferForPath)97 protected static @ClassName("android.graphics.FontFamily") Object makeFamilyFromParsed( 98 @ClassName("android.text.FontConfig$Family") Object family, 99 Map<String, ByteBuffer> bufferForPath) { 100 FontConfigFamilyReflector reflector = reflector(FontConfigFamilyReflector.class, family); 101 102 FontFamily fontFamily = 103 Shadow.newInstance( 104 FontFamily.class, 105 new Class<?>[] {String.class, int.class}, 106 new Object[] {reflector.getLanguage(), reflector.getVariant()}); 107 for (FontConfig.Font font : reflector.getFonts()) { 108 String fullPathName = 109 System.getProperty("robolectric.nativeruntime.fontdir") 110 + reflector(FontConfigFontReflector.class, font).getFontName(); 111 ByteBuffer fontBuffer = bufferForPath.get(fullPathName); 112 if (fontBuffer == null) { 113 try (FileInputStream file = new FileInputStream(fullPathName)) { 114 FileChannel fileChannel = file.getChannel(); 115 long fontSize = fileChannel.size(); 116 fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); 117 bufferForPath.put(fullPathName, fontBuffer); 118 } catch (IOException e) { 119 Log.w(TAG, "Error mapping font file " + fullPathName); 120 continue; 121 } 122 } 123 if (!fontFamily.addFontFromBuffer( 124 fontBuffer, 125 font.getTtcIndex(), 126 font.getAxes(), 127 font.getWeight(), 128 font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) { 129 Log.e(TAG, "Error creating font " + fullPathName + "#" + font.getTtcIndex()); 130 } 131 } 132 if (!fontFamily.freeze()) { 133 // Treat as system error since reaching here means that a system pre-installed font 134 // can't be used by our font stack. 135 Log.w(TAG, "Unable to load Family: " + reflector.getName() + ":" + reflector.getLanguage()); 136 return null; 137 } 138 return fontFamily; 139 } 140 141 @Implementation(maxSdk = U.SDK_INT) nativeCreateFromTypeface(long nativeInstance, int style)142 protected static long nativeCreateFromTypeface(long nativeInstance, int style) { 143 return TypefaceNatives.nativeCreateFromTypeface(nativeInstance, style); 144 } 145 146 @Implementation(minSdk = O, maxSdk = U.SDK_INT) nativeCreateFromTypefaceWithExactStyle( long nativeInstance, int weight, boolean italic)147 protected static long nativeCreateFromTypefaceWithExactStyle( 148 long nativeInstance, int weight, boolean italic) { 149 return TypefaceNatives.nativeCreateFromTypefaceWithExactStyle(nativeInstance, weight, italic); 150 } 151 152 @Implementation(minSdk = O, maxSdk = U.SDK_INT) nativeCreateFromTypefaceWithVariation( long nativeInstance, List<FontVariationAxis> axes)153 protected static long nativeCreateFromTypefaceWithVariation( 154 long nativeInstance, List<FontVariationAxis> axes) { 155 return TypefaceNatives.nativeCreateFromTypefaceWithVariation(nativeInstance, axes); 156 } 157 158 @Implementation(maxSdk = U.SDK_INT) nativeCreateWeightAlias(long nativeInstance, int weight)159 protected static long nativeCreateWeightAlias(long nativeInstance, int weight) { 160 return TypefaceNatives.nativeCreateWeightAlias(nativeInstance, weight); 161 } 162 163 @Implementation(minSdk = O, maxSdk = R) nativeCreateFromArray(long[] familyArray, int weight, int italic)164 protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) { 165 return TypefaceNatives.nativeCreateFromArray(familyArray, 0, weight, italic); 166 } 167 168 @Implementation(minSdk = S, maxSdk = U.SDK_INT) nativeCreateFromArray( long[] familyArray, long fallbackTypeface, int weight, int italic)169 protected static long nativeCreateFromArray( 170 long[] familyArray, long fallbackTypeface, int weight, int italic) { 171 return TypefaceNatives.nativeCreateFromArray(familyArray, fallbackTypeface, weight, italic); 172 } 173 174 @Implementation(minSdk = O, maxSdk = U.SDK_INT) nativeGetSupportedAxes(long nativeInstance)175 protected static int[] nativeGetSupportedAxes(long nativeInstance) { 176 return TypefaceNatives.nativeGetSupportedAxes(nativeInstance); 177 } 178 179 @Implementation(maxSdk = U.SDK_INT) nativeSetDefault(long nativePtr)180 protected static void nativeSetDefault(long nativePtr) { 181 TypefaceNatives.nativeSetDefault(nativePtr); 182 } 183 184 @Implementation(maxSdk = U.SDK_INT) nativeGetStyle(long nativePtr)185 protected static int nativeGetStyle(long nativePtr) { 186 return TypefaceNatives.nativeGetStyle(nativePtr); 187 } 188 189 @Implementation(minSdk = O, maxSdk = U.SDK_INT) nativeGetWeight(long nativePtr)190 protected static int nativeGetWeight(long nativePtr) { 191 return TypefaceNatives.nativeGetWeight(nativePtr); 192 } 193 194 @Implementation(minSdk = P, maxSdk = U.SDK_INT) nativeGetReleaseFunc()195 protected static long nativeGetReleaseFunc() { 196 DefaultNativeRuntimeLoader.injectAndLoad(); 197 return TypefaceNatives.nativeGetReleaseFunc(); 198 } 199 200 @Implementation(minSdk = S, maxSdk = TIRAMISU) nativeGetFamilySize(long nativePtr)201 protected static int nativeGetFamilySize(long nativePtr) { 202 return TypefaceNatives.nativeGetFamilySize(nativePtr); 203 } 204 205 @Implementation(minSdk = S, maxSdk = TIRAMISU) nativeGetFamily(long nativePtr, int index)206 protected static long nativeGetFamily(long nativePtr, int index) { 207 return TypefaceNatives.nativeGetFamily(nativePtr, index); 208 } 209 210 @Implementation(minSdk = AndroidVersions.Baklava.SDK_INT) 211 @InDevelopment nativeIsVariationInstance(long nativePtr)212 protected static boolean nativeIsVariationInstance(long nativePtr) { 213 return false; 214 // TODO: call the real impl when it's finally available in native binaries 215 // return TypefaceNatives.nativeIsVariationInstance(nativePtr); 216 } 217 218 @Implementation(minSdk = Q, maxSdk = U.SDK_INT) nativeRegisterGenericFamily(String str, long nativePtr)219 protected static void nativeRegisterGenericFamily(String str, long nativePtr) { 220 TypefaceNatives.nativeRegisterGenericFamily(str, nativePtr); 221 } 222 223 @Implementation(minSdk = S, maxSdk = TIRAMISU) nativeWriteTypefaces(ByteBuffer buffer, long[] nativePtrs)224 protected static int nativeWriteTypefaces(ByteBuffer buffer, long[] nativePtrs) { 225 return TypefaceNatives.nativeWriteTypefaces(buffer, nativePtrs); 226 } 227 228 @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) nativeWriteTypefaces(ByteBuffer buffer, int position, long[] nativePtrs)229 protected static int nativeWriteTypefaces(ByteBuffer buffer, int position, long[] nativePtrs) { 230 return nativeWriteTypefaces(buffer, nativePtrs); 231 } 232 233 @Implementation(minSdk = S, maxSdk = TIRAMISU) nativeReadTypefaces(ByteBuffer buffer)234 protected static long[] nativeReadTypefaces(ByteBuffer buffer) { 235 return TypefaceNatives.nativeReadTypefaces(buffer); 236 } 237 238 @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) nativeReadTypefaces(ByteBuffer buffer, int position)239 protected static long[] nativeReadTypefaces(ByteBuffer buffer, int position) { 240 return nativeReadTypefaces(buffer); 241 } 242 243 @Implementation(minSdk = S, maxSdk = U.SDK_INT) nativeForceSetStaticFinalField(String fieldName, Typeface typeface)244 protected static void nativeForceSetStaticFinalField(String fieldName, Typeface typeface) { 245 TypefaceNatives.nativeForceSetStaticFinalField(fieldName, typeface); 246 } 247 248 @Implementation(minSdk = S, maxSdk = U.SDK_INT) nativeAddFontCollections(long nativePtr)249 protected static void nativeAddFontCollections(long nativePtr) { 250 TypefaceNatives.nativeAddFontCollections(nativePtr); 251 } 252 253 @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT) nativeRegisterLocaleList(String locales)254 protected static void nativeRegisterLocaleList(String locales) { 255 // no-op 256 } 257 ensureInitialized()258 static void ensureInitialized() { 259 try { 260 // Forces static initialization. This should be called before any native code that calls 261 // Typeface::resolveDefault. 262 Class.forName("android.graphics.Typeface"); 263 } catch (ClassNotFoundException e) { 264 throw new LinkageError("Unable to load Typeface", e); 265 } 266 } 267 268 @Override getFontDescription()269 public FontDesc getFontDescription() { 270 throw new UnsupportedOperationException( 271 "Legacy ShadowTypeface description APIs are not supported"); 272 } 273 274 /** 275 * Shadow for {@link Typeface.Builder}. It is empty to avoid using the legacy {@link 276 * Typeface.Builder} shadow. 277 */ 278 @Implements( 279 value = Typeface.Builder.class, 280 minSdk = P, 281 shadowPicker = ShadowNativeTypefaceBuilder.Picker.class, 282 isInAndroidSdk = false) 283 public static class ShadowNativeTypefaceBuilder { 284 /** Shadow picker for {@link Typeface.Builder}. */ 285 public static final class Picker extends GraphicsShadowPicker<Object> { Picker()286 public Picker() { 287 super(ShadowLegacyTypeface.ShadowBuilder.class, ShadowNativeTypefaceBuilder.class); 288 } 289 } 290 } 291 292 @ForType(Typeface.class) 293 interface TypefaceReflector { 294 @CanIgnoreReturnValue 295 @Static 296 @Direct buildSystemFallback( String xmlPath, String fontDir, ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap)297 FontConfig.Alias[] buildSystemFallback( 298 String xmlPath, 299 String fontDir, 300 ArrayMap<String, Typeface> fontMap, 301 ArrayMap<String, FontFamily[]> fallbackMap); 302 } 303 304 @ForType(className = "android.text.FontConfig$Family") 305 interface FontConfigFamilyReflector { getLanguage()306 String getLanguage(); 307 getVariant()308 int getVariant(); 309 getFonts()310 FontConfig.Font[] getFonts(); 311 getName()312 String getName(); 313 } 314 315 @ForType(className = "android.text.FontConfig$Font") 316 interface FontConfigFontReflector { getFontName()317 String getFontName(); 318 } 319 } 320