1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.robolectric.android.plugins;
18 
19 import static android.os.Build.VERSION_CODES.O;
20 
21 import static com.google.common.base.StandardSystemProperty.OS_ARCH;
22 import static com.google.common.base.StandardSystemProperty.OS_NAME;
23 
24 import static org.robolectric.util.reflector.Reflector.reflector;
25 
26 import android.graphics.Typeface;
27 import android.os.Build;
28 
29 import com.google.auto.service.AutoService;
30 import com.google.common.collect.ImmutableList;
31 import com.google.common.io.Files;
32 import com.google.common.io.Resources;
33 
34 import org.robolectric.internal.bytecode.ShadowConstants;
35 import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
36 import org.robolectric.pluginapi.NativeRuntimeLoader;
37 import org.robolectric.shadow.api.Shadow;
38 import org.robolectric.util.PerfStatsCollector;
39 import org.robolectric.util.ReflectionHelpers;
40 import org.robolectric.util.TempDirectory;
41 import org.robolectric.util.inject.Supercedes;
42 import org.robolectric.util.reflector.Accessor;
43 import org.robolectric.util.reflector.ForType;
44 import org.robolectric.versioning.AndroidVersions;
45 import org.robolectric.versioning.AndroidVersions.U;
46 import org.robolectric.versioning.AndroidVersions.V;
47 
48 import java.io.IOException;
49 import java.net.URL;
50 import java.nio.file.Path;
51 import java.util.Locale;
52 import java.util.Objects;
53 
54 import javax.annotation.Priority;
55 
56 /** Loads the Robolectric native runtime. */
57 @AutoService(NativeRuntimeLoader.class)
58 @Supercedes(DefaultNativeRuntimeLoader.class)
59 @Priority(Integer.MIN_VALUE)
60 public class AndroidNativeRuntimeLoader extends DefaultNativeRuntimeLoader {
61   private static final String METHOD_BINDING_FORMAT = "$$robo$$${method}$nativeBinding";
62 
63   // Core classes for which native methods are to be registered.
64   private static final ImmutableList<String> CORE_CLASS_NATIVES =
65       ImmutableList.copyOf(
66           new String[] {
67             "android.animation.PropertyValuesHolder",
68             "android.database.CursorWindow",
69             "android.database.sqlite.SQLiteConnection",
70             "android.media.ImageReader",
71             "android.view.Surface",
72             "com.android.internal.util.VirtualRefBasePtr",
73             "libcore.util.NativeAllocationRegistry",
74           });
75 
76   // Graphics classes for which native methods are to be registered.
77   private static final ImmutableList<String> GRAPHICS_CLASS_NATIVES =
78       ImmutableList.copyOf(
79           new String[] {
80             "android.graphics.Bitmap",
81             "android.graphics.BitmapFactory",
82             "android.graphics.ByteBufferStreamAdaptor",
83             "android.graphics.Camera",
84             "android.graphics.Canvas",
85             "android.graphics.CanvasProperty",
86             "android.graphics.Color",
87             "android.graphics.ColorFilter",
88             "android.graphics.ColorSpace",
89             "android.graphics.CreateJavaOutputStreamAdaptor",
90             "android.graphics.DrawFilter",
91             "android.graphics.FontFamily",
92             "android.graphics.Gainmap",
93             "android.graphics.Graphics",
94             "android.graphics.HardwareRenderer",
95             "android.graphics.HardwareRendererObserver",
96             "android.graphics.ImageDecoder",
97             "android.graphics.Interpolator",
98             "android.graphics.MaskFilter",
99             "android.graphics.Matrix",
100             "android.graphics.NinePatch",
101             "android.graphics.Paint",
102             "android.graphics.Path",
103             "android.graphics.PathEffect",
104             "android.graphics.PathMeasure",
105             "android.graphics.Picture",
106             "android.graphics.RecordingCanvas",
107             "android.graphics.Region",
108             "android.graphics.RenderEffect",
109             "android.graphics.RenderNode",
110             "android.graphics.Shader",
111             "android.graphics.Typeface",
112             "android.graphics.YuvImage",
113             "android.graphics.animation.NativeInterpolatorFactory",
114             "android.graphics.animation.RenderNodeAnimator",
115             "android.graphics.drawable.AnimatedVectorDrawable",
116             "android.graphics.drawable.AnimatedImageDrawable",
117             "android.graphics.drawable.VectorDrawable",
118             "android.graphics.fonts.Font",
119             "android.graphics.fonts.FontFamily",
120             "android.graphics.text.LineBreaker",
121             "android.graphics.text.MeasuredText",
122             "android.graphics.text.TextRunShaper",
123             "android.util.PathParser",
124           });
125 
126   /**
127    * {@link #DEFERRED_STATIC_INITIALIZERS} that invoke their own native methods in static
128    * initializers. Unlike libcore, registering JNI on the JVM causes static initialization to be
129    * performed on the class. Because of this, static initializers cannot invoke the native methods
130    * of the class under registration. Executing these static initializers must be deferred until
131    * after JNI has been registered.
132    */
133   private static final ImmutableList<String> DEFERRED_STATIC_INITIALIZERS =
134       ImmutableList.copyOf(
135           new String[] {
136             "android.graphics.FontFamily",
137             "android.graphics.Path",
138             "android.graphics.Typeface",
139             "android.graphics.text.MeasuredText$Builder",
140             "android.media.ImageReader",
141           });
142 
143   @Override
ensureLoaded()144   public synchronized void ensureLoaded() {
145     DefaultNativeRuntimeLoaderReflector accessor = reflector(DefaultNativeRuntimeLoaderReflector.class, this);
146     if (loaded.get()) {
147       return;
148     }
149 
150     if (!accessor.isSupported()) {
151       String errorMessage =
152           String.format(
153               "The Robolectric native runtime is not supported on %s (%s)",
154               OS_NAME.value(), OS_ARCH.value());
155       throw new AssertionError(errorMessage);
156     }
157     loaded.set(true);
158 
159     try {
160       PerfStatsCollector.getInstance()
161           .measure(
162               "loadNativeRuntime",
163               () -> {
164                 TempDirectory extractDirectory = new TempDirectory("nativeruntime");
165                 accessor.setExtractDirectory(extractDirectory);
166                 System.setProperty("icu.locale.default", Locale.getDefault().toLanguageTag());
167                 if (Build.VERSION.SDK_INT >= O) {
168                   accessor.maybeCopyFonts(extractDirectory);
169                 }
170                 maybeCopyIcuData(extractDirectory);
171                 maybeCopyArscFile(extractDirectory);
172                 if (isAndroidVOrAbove()) {
173                   // Load per-sdk Robolectric Native Runtime (RNR)
174                   System.setProperty("core_native_classes", String.join(",", CORE_CLASS_NATIVES));
175                   System.setProperty(
176                       "graphics_native_classes", String.join(",", GRAPHICS_CLASS_NATIVES));
177                   System.setProperty("method_binding_format", METHOD_BINDING_FORMAT);
178                 }
179                 loadLibrary(extractDirectory);
180                 if (isAndroidVOrAbove()) {
181                   invokeDeferredStaticInitializers();
182                   Typeface.loadPreinstalledSystemFontMap();
183                 }
184               });
185     } catch (IOException e) {
186       throw new AssertionError("Unable to load Robolectric native runtime library", e);
187     }
188   }
189 
190   /** Attempts to load the ICU dat file. This is only relevant for native graphics. */
maybeCopyIcuData(TempDirectory tempDirectory)191   private void maybeCopyIcuData(TempDirectory tempDirectory) throws IOException {
192     String icuDatFile = isAndroidVOrAbove() ? "icudt.dat" : "icudt68l.dat";
193 
194     URL icuDatUrl;
195     try {
196       icuDatUrl = Resources.getResource("icu/" + icuDatFile);
197     } catch (IllegalArgumentException e) {
198       return;
199     }
200     Path icuPath = tempDirectory.create("icu");
201     Path icuDatPath = icuPath.resolve(icuDatFile);
202     Resources.asByteSource(icuDatUrl).copyTo(Files.asByteSink(icuDatPath.toFile()));
203     System.setProperty("icu.data.path", icuDatPath.toAbsolutePath().toString());
204   }
205 
206   /** Attempts to load the ARSC file. This is only relevant for native graphics. */
maybeCopyArscFile(TempDirectory tempDirectory)207   private void maybeCopyArscFile(TempDirectory tempDirectory) throws IOException {
208     URL arscUrl;
209     final String arscFileName = "font_resources.arsc";
210     try {
211       arscUrl = Resources.getResource(arscFileName);
212     } catch (IllegalArgumentException e) {
213       return;
214     }
215     Path arscPath = tempDirectory.create("arsc");
216     Path arscFilePath = arscPath.resolve(arscFileName);
217     Resources.asByteSource(arscUrl).copyTo(Files.asByteSink(arscFilePath.toFile()));
218     System.setProperty("arsc.file.path", arscFilePath.toAbsolutePath().toString());
219   }
220 
invokeDeferredStaticInitializers()221   private void invokeDeferredStaticInitializers() {
222     for (String className : DEFERRED_STATIC_INITIALIZERS) {
223       ReflectionHelpers.callStaticMethod(
224               Shadow.class.getClassLoader(), className, ShadowConstants.STATIC_INITIALIZER_METHOD_NAME);
225     }
226   }
227 
loadLibrary(TempDirectory tempDirectory)228   private void loadLibrary(TempDirectory tempDirectory) throws IOException {
229     String libraryName = System.mapLibraryName("robolectric-nativeruntime");
230     Path libraryPath = tempDirectory.getBasePath().resolve(libraryName);
231     URL libraryResource = Resources.getResource(nativeLibraryPath());
232     Resources.asByteSource(libraryResource).copyTo(Files.asByteSink(libraryPath.toFile()));
233     System.load(libraryPath.toAbsolutePath().toString());
234   }
235 
236   /** For V and above, insert "V" folder for V and above lib path. */
nativeLibraryPath()237   private String nativeLibraryPath() {
238     DefaultNativeRuntimeLoaderReflector accessor =
239             reflector(DefaultNativeRuntimeLoaderReflector.class, this);
240     String defaultPath = accessor.nativeLibraryPath();
241     if (isAndroidVOrAbove()) {
242       int index = defaultPath.lastIndexOf(System.mapLibraryName("robolectric-nativeruntime"));
243       if (index < 0) {
244         return defaultPath;
245       }
246       String result = defaultPath.substring(0,index) + "V/" + defaultPath.substring(index);
247       return result;
248     }
249     return defaultPath;
250   }
251 
isAndroidVOrAbove()252   private boolean isAndroidVOrAbove() {
253     return Objects.equals(AndroidVersions.CURRENT.getShortCode(), V.SHORT_CODE) &&
254             AndroidVersions.CURRENT.getSdkInt() >= U.SDK_INT;
255   }
256 
257   @ForType(DefaultNativeRuntimeLoader.class)
258   private interface DefaultNativeRuntimeLoaderReflector {
259     @Accessor("extractDirectory")
setExtractDirectory(TempDirectory dir)260     void setExtractDirectory(TempDirectory dir);
261 
isSupported()262     boolean isSupported();
maybeCopyFonts(TempDirectory tempDirectory)263     void maybeCopyFonts(TempDirectory tempDirectory);
nativeLibraryPath()264     String nativeLibraryPath();
265   }
266 }
267