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