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