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