1 package org.robolectric.nativeruntime; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static com.google.common.base.StandardSystemProperty.OS_ARCH; 5 import static com.google.common.base.StandardSystemProperty.OS_NAME; 6 7 import android.database.CursorWindow; 8 import android.graphics.Typeface; 9 import android.os.Build; 10 import com.google.auto.service.AutoService; 11 import com.google.common.annotations.VisibleForTesting; 12 import com.google.common.collect.ImmutableList; 13 import com.google.common.collect.ImmutableMap; 14 import com.google.common.io.Files; 15 import com.google.common.io.Resources; 16 import java.io.File; 17 import java.io.IOException; 18 import java.net.URI; 19 import java.net.URISyntaxException; 20 import java.net.URL; 21 import java.nio.file.FileSystem; 22 import java.nio.file.FileSystems; 23 import java.nio.file.Path; 24 import java.nio.file.Paths; 25 import java.util.Iterator; 26 import java.util.Locale; 27 import java.util.Objects; 28 import java.util.concurrent.atomic.AtomicBoolean; 29 import java.util.concurrent.atomic.AtomicReference; 30 import java.util.stream.Stream; 31 import javax.annotation.Priority; 32 import org.robolectric.pluginapi.NativeRuntimeLoader; 33 import org.robolectric.shadow.api.Shadow; 34 import org.robolectric.util.OsUtil; 35 import org.robolectric.util.PerfStatsCollector; 36 import org.robolectric.util.ReflectionHelpers; 37 import org.robolectric.util.TempDirectory; 38 import org.robolectric.util.inject.Injector; 39 40 /** Loads the Robolectric native runtime. */ 41 @AutoService(NativeRuntimeLoader.class) 42 @Priority(Integer.MIN_VALUE) 43 public class DefaultNativeRuntimeLoader implements NativeRuntimeLoader { 44 protected static final AtomicBoolean loaded = new AtomicBoolean(false); 45 46 private static final AtomicReference<NativeRuntimeLoader> nativeRuntimeLoader = 47 new AtomicReference<>(); 48 49 protected static final String METHOD_BINDING_FORMAT = "$$robo$$${method}$nativeBinding"; 50 51 // Core classes for which native methods are to be registered for Android V and above. 52 protected static final ImmutableList<String> CORE_CLASS_NATIVES = 53 ImmutableList.copyOf( 54 new String[] { 55 "android.animation.PropertyValuesHolder", 56 "android.database.CursorWindow", 57 "android.database.sqlite.SQLiteConnection", 58 "android.database.sqlite.SQLiteRawStatement", 59 "android.media.ImageReader", 60 "android.view.Surface", 61 "com.android.internal.util.VirtualRefBasePtr", 62 "libcore.util.NativeAllocationRegistry", 63 }); 64 65 // Graphics classes for which native methods are to be registered. 66 protected static final ImmutableList<String> GRAPHICS_CLASS_NATIVES = 67 ImmutableList.copyOf( 68 new String[] { 69 "android.graphics.Bitmap", 70 "android.graphics.BitmapFactory", 71 "android.graphics.ByteBufferStreamAdaptor", 72 "android.graphics.Camera", 73 "android.graphics.Canvas", 74 "android.graphics.CanvasProperty", 75 "android.graphics.Color", 76 "android.graphics.ColorFilter", 77 "android.graphics.ColorSpace", 78 "android.graphics.CreateJavaOutputStreamAdaptor", 79 "android.graphics.DrawFilter", 80 "android.graphics.FontFamily", 81 "android.graphics.Gainmap", 82 "android.graphics.Graphics", 83 "android.graphics.HardwareRenderer", 84 "android.graphics.HardwareRendererObserver", 85 "android.graphics.ImageDecoder", 86 "android.graphics.Interpolator", 87 "android.graphics.MaskFilter", 88 "android.graphics.Matrix", 89 "android.graphics.NinePatch", 90 "android.graphics.Paint", 91 "android.graphics.Path", 92 "android.graphics.PathEffect", 93 "android.graphics.PathIterator", 94 "android.graphics.PathMeasure", 95 "android.graphics.Picture", 96 "android.graphics.RecordingCanvas", 97 "android.graphics.Region", 98 "android.graphics.RenderEffect", 99 "android.graphics.RenderNode", 100 "android.graphics.Shader", 101 "android.graphics.Typeface", 102 "android.graphics.YuvImage", 103 "android.graphics.animation.NativeInterpolatorFactory", 104 "android.graphics.animation.RenderNodeAnimator", 105 "android.graphics.drawable.AnimatedVectorDrawable", 106 "android.graphics.drawable.AnimatedImageDrawable", 107 "android.graphics.drawable.VectorDrawable", 108 "android.graphics.fonts.Font", 109 "android.graphics.fonts.FontFamily", 110 "android.graphics.text.LineBreaker", 111 "android.graphics.text.MeasuredText", 112 "android.graphics.text.TextRunShaper", 113 "android.util.PathParser", 114 }); 115 116 /** 117 * {@link #DEFERRED_STATIC_INITIALIZERS} that invoke their own native methods in static 118 * initializers. Unlike libcore, registering JNI on the JVM causes static initialization to be 119 * performed on the class. Because of this, static initializers cannot invoke the native methods 120 * of the class under registration. Executing these static initializers must be deferred until 121 * after JNI has been registered. 122 */ 123 protected static final ImmutableList<String> DEFERRED_STATIC_INITIALIZERS = 124 ImmutableList.copyOf( 125 new String[] { 126 "android.graphics.FontFamily", 127 "android.graphics.Path", 128 "android.graphics.PathIterator", 129 "android.graphics.Typeface", 130 "android.graphics.text.MeasuredText$Builder", 131 "android.media.ImageReader", 132 }); 133 134 private TempDirectory extractDirectory; 135 injectAndLoad()136 public static void injectAndLoad() { 137 // Ensure a single instance. 138 synchronized (nativeRuntimeLoader) { 139 if (nativeRuntimeLoader.get() == null) { 140 Injector injector = new Injector.Builder(CursorWindow.class.getClassLoader()).build(); 141 NativeRuntimeLoader loader = injector.getInstance(NativeRuntimeLoader.class); 142 nativeRuntimeLoader.set(loader); 143 } 144 } 145 nativeRuntimeLoader.get().ensureLoaded(); 146 } 147 148 @Override ensureLoaded()149 public synchronized void ensureLoaded() { 150 if (loaded.get()) { 151 return; 152 } 153 154 if (!isSupported()) { 155 String errorMessage = 156 String.format( 157 "The Robolectric native runtime is not supported on %s (%s)", 158 OS_NAME.value(), OS_ARCH.value()); 159 throw new AssertionError(errorMessage); 160 } 161 loaded.set(true); 162 163 try { 164 PerfStatsCollector.getInstance() 165 .measure( 166 "loadNativeRuntime", 167 () -> { 168 extractDirectory = new TempDirectory("nativeruntime"); 169 if (Build.VERSION.SDK_INT >= O) { 170 // Only copy fonts if graphics is supported, not just SQLite. 171 maybeCopyFonts(extractDirectory); 172 } 173 maybeCopyIcuData(extractDirectory); 174 if (isAndroidVOrGreater()) { 175 System.setProperty("core_native_classes", String.join(",", CORE_CLASS_NATIVES)); 176 System.setProperty( 177 "graphics_native_classes", String.join(",", GRAPHICS_CLASS_NATIVES)); 178 System.setProperty("method_binding_format", METHOD_BINDING_FORMAT); 179 } 180 loadLibrary(extractDirectory); 181 if (isAndroidVOrGreater()) { 182 invokeDeferredStaticInitializers(); 183 Typeface.loadPreinstalledSystemFontMap(); 184 } 185 }); 186 } catch (IOException e) { 187 throw new AssertionError("Unable to load Robolectric native runtime library", e); 188 } 189 } 190 191 /** Attempts to load the ICU dat file. This is only relevant for native graphics. */ maybeCopyIcuData(TempDirectory tempDirectory)192 private void maybeCopyIcuData(TempDirectory tempDirectory) throws IOException { 193 URL icuDatUrl; 194 try { 195 icuDatUrl = 196 Resources.getResource(isAndroidVOrGreater() ? "icu/icudt75l.dat" : "icu/icudt68l.dat"); 197 } catch (IllegalArgumentException e) { 198 return; 199 } 200 Path icuPath = tempDirectory.create("icu"); 201 Path icuDatPath = icuPath.resolve(isAndroidVOrGreater() ? "icudt75l.dat" : "icudt68l.dat"); 202 Resources.asByteSource(icuDatUrl).copyTo(Files.asByteSink(icuDatPath.toFile())); 203 System.setProperty("icu.data.path", icuDatPath.toAbsolutePath().toString()); 204 System.setProperty("icu.locale.default", Locale.getDefault().toLanguageTag()); 205 } 206 207 /** 208 * Attempts to copy the system fonts to a temporary directory. This is only relevant for native 209 * graphics. 210 */ maybeCopyFonts(TempDirectory tempDirectory)211 private void maybeCopyFonts(TempDirectory tempDirectory) throws IOException { 212 URI fontsUri = null; 213 try { 214 fontsUri = Resources.getResource("fonts/").toURI(); 215 } catch (IllegalArgumentException | URISyntaxException e) { 216 return; 217 } 218 219 FileSystem zipfs = null; 220 221 if ("jar".equals(fontsUri.getScheme())) { 222 zipfs = FileSystems.newFileSystem(fontsUri, ImmutableMap.of("create", "true")); 223 } 224 225 Path fontsInputPath = Paths.get(fontsUri); 226 Path fontsOutputPath = tempDirectory.create("fonts"); 227 228 try (Stream<Path> pathStream = java.nio.file.Files.walk(fontsInputPath)) { 229 Iterator<Path> fileIterator = pathStream.iterator(); 230 while (fileIterator.hasNext()) { 231 Path path = fileIterator.next(); 232 // Avoid copying parent directory. 233 if ("fonts".equals(path.getFileName().toString())) { 234 continue; 235 } 236 String fontPath = "fonts/" + path.getFileName(); 237 URL resource = Resources.getResource(fontPath); 238 Path outputPath = tempDirectory.getBasePath().resolve(fontPath); 239 Resources.asByteSource(resource).copyTo(Files.asByteSink(outputPath.toFile())); 240 } 241 } 242 System.setProperty( 243 "robolectric.nativeruntime.fontdir", 244 // Android's FontListParser expects a trailing slash for the base font directory. 245 fontsOutputPath.toAbsolutePath() + File.separator); 246 if (zipfs != null) { 247 zipfs.close(); 248 } 249 } 250 loadLibrary(TempDirectory tempDirectory)251 private void loadLibrary(TempDirectory tempDirectory) throws IOException { 252 Path libraryPath = tempDirectory.getBasePath().resolve(libraryName()); 253 URL libraryResource = Resources.getResource(nativeLibraryPath()); 254 Resources.asByteSource(libraryResource).copyTo(Files.asByteSink(libraryPath.toFile())); 255 System.load(libraryPath.toAbsolutePath().toString()); 256 } 257 isSupported()258 private static boolean isSupported() { 259 return (OsUtil.isMac() 260 && (Objects.equals(arch(), "aarch64") || Objects.equals(arch(), "x86_64"))) 261 || (OsUtil.isLinux() && Objects.equals(arch(), "x86_64")) 262 || (OsUtil.isWindows() && Objects.equals(arch(), "x86_64")); 263 } 264 nativeLibraryPath()265 private static String nativeLibraryPath() { 266 return String.format("native/%s/%s/%s", osName(), arch(), libraryName()); 267 } 268 libraryName()269 protected static String libraryName() { 270 if (isAndroidVOrGreater()) { 271 // For V and above, hwui's android_graphics_HardwareRenderer.cpp has shared library symbol 272 // lookup logic that assumes that Windows library name is "libandroid_runtime.dll". 273 return System.mapLibraryName(OsUtil.isWindows() ? "libandroid_runtime" : "android_runtime"); 274 } else { 275 return System.mapLibraryName("robolectric-nativeruntime"); 276 } 277 } 278 osName()279 private static String osName() { 280 if (OsUtil.isLinux()) { 281 return "linux"; 282 } else if (OsUtil.isMac()) { 283 return "mac"; 284 } else if (OsUtil.isWindows()) { 285 return "windows"; 286 } 287 return "unknown"; 288 } 289 arch()290 private static String arch() { 291 String arch = OS_ARCH.value().toLowerCase(Locale.US); 292 if (arch.equals("x86_64") || arch.equals("amd64")) { 293 return "x86_64"; 294 } 295 return arch; 296 } 297 298 @VisibleForTesting isLoaded()299 static boolean isLoaded() { 300 return loaded.get(); 301 } 302 303 @VisibleForTesting getDirectory()304 Path getDirectory() { 305 return extractDirectory == null ? null : extractDirectory.getBasePath(); 306 } 307 308 @VisibleForTesting resetLoaded()309 static void resetLoaded() { 310 loaded.set(false); 311 } 312 invokeDeferredStaticInitializers()313 protected void invokeDeferredStaticInitializers() { 314 for (String className : DEFERRED_STATIC_INITIALIZERS) { 315 ReflectionHelpers.callStaticMethod( 316 Shadow.class.getClassLoader(), className, "__staticInitializer__"); 317 } 318 } 319 isAndroidVOrGreater()320 private static boolean isAndroidVOrGreater() { 321 return Build.VERSION.SDK_INT >= /* VANILLA_ICE_CREAM */ 35; 322 } 323 } 324