• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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