1 package org.robolectric.android.internal;
2 
3 import static android.location.LocationManager.GPS_PROVIDER;
4 import static android.os.Build.VERSION_CODES.P;
5 import static org.robolectric.shadow.api.Shadow.newInstanceOf;
6 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
7 
8 import android.annotation.SuppressLint;
9 import android.app.ActivityThread;
10 import android.app.Application;
11 import android.app.IInstrumentationWatcher;
12 import android.app.IUiAutomationConnection;
13 import android.app.Instrumentation;
14 import android.app.LoadedApk;
15 import android.content.BroadcastReceiver;
16 import android.content.ComponentName;
17 import android.content.Context;
18 import android.content.IntentFilter;
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageManager;
21 import android.content.pm.PackageParser;
22 import android.content.res.AssetManager;
23 import android.content.res.Configuration;
24 import android.content.res.Resources;
25 import android.os.Build;
26 import android.os.Build.VERSION_CODES;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.provider.Settings.Secure;
31 import android.util.DisplayMetrics;
32 import com.google.common.annotations.VisibleForTesting;
33 import java.lang.reflect.Method;
34 import java.security.Security;
35 import java.util.Locale;
36 import org.bouncycastle.jce.provider.BouncyCastleProvider;
37 import org.robolectric.ApkLoader;
38 import org.robolectric.RuntimeEnvironment;
39 import org.robolectric.android.Bootstrap;
40 import org.robolectric.android.fakes.RoboMonitoringInstrumentation;
41 import org.robolectric.annotation.Config;
42 import org.robolectric.internal.ParallelUniverseInterface;
43 import org.robolectric.internal.SdkConfig;
44 import org.robolectric.internal.SdkEnvironment;
45 import org.robolectric.manifest.AndroidManifest;
46 import org.robolectric.manifest.BroadcastReceiverData;
47 import org.robolectric.manifest.RoboNotFoundException;
48 import org.robolectric.res.FsFile;
49 import org.robolectric.res.PackageResourceTable;
50 import org.robolectric.res.ResourceTable;
51 import org.robolectric.res.RoutingResourceTable;
52 import org.robolectric.shadow.api.Shadow;
53 import org.robolectric.shadows.ClassNameResolver;
54 import org.robolectric.shadows.LegacyManifestParser;
55 import org.robolectric.shadows.ShadowActivityThread;
56 import org.robolectric.shadows.ShadowApplication;
57 import org.robolectric.shadows.ShadowAssetManager;
58 import org.robolectric.shadows.ShadowContextImpl;
59 import org.robolectric.shadows.ShadowLog;
60 import org.robolectric.shadows.ShadowLooper;
61 import org.robolectric.shadows.ShadowPackageManager;
62 import org.robolectric.shadows.ShadowPackageParser;
63 import org.robolectric.util.PerfStatsCollector;
64 import org.robolectric.util.ReflectionHelpers;
65 import org.robolectric.util.Scheduler;
66 import org.robolectric.util.TempDirectory;
67 
68 @SuppressLint("NewApi")
69 public class ParallelUniverse implements ParallelUniverseInterface {
70 
71   private boolean loggingInitialized = false;
72   private SdkConfig sdkConfig;
73 
74   @Override
setSdkConfig(SdkConfig sdkConfig)75   public void setSdkConfig(SdkConfig sdkConfig) {
76     this.sdkConfig = sdkConfig;
77     ReflectionHelpers.setStaticField(RuntimeEnvironment.class, "apiLevel", sdkConfig.getApiLevel());
78   }
79 
80   @Override
setResourcesMode(boolean legacyResources)81   public void setResourcesMode(boolean legacyResources) {
82     RuntimeEnvironment.setUseLegacyResources(legacyResources);
83   }
84 
85   @Override
setUpApplicationState(ApkLoader apkLoader, Method method, Config config, AndroidManifest appManifest, SdkEnvironment sdkEnvironment)86   public void setUpApplicationState(ApkLoader apkLoader, Method method, Config config,
87       AndroidManifest appManifest, SdkEnvironment sdkEnvironment) {
88     ReflectionHelpers.setStaticField(RuntimeEnvironment.class, "apiLevel", sdkConfig.getApiLevel());
89 
90     RuntimeEnvironment.application = null;
91     RuntimeEnvironment.setActivityThread(null);
92     RuntimeEnvironment.setTempDirectory(new TempDirectory(createTestDataDirRootPath(method)));
93     RuntimeEnvironment.setMasterScheduler(new Scheduler());
94     RuntimeEnvironment.setMainThread(Thread.currentThread());
95 
96     if (!loggingInitialized) {
97       ShadowLog.setupLogging();
98       loggingInitialized = true;
99     }
100 
101     if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
102       Security.insertProviderAt(new BouncyCastleProvider(), 1);
103     }
104 
105     Configuration configuration = new Configuration();
106     DisplayMetrics displayMetrics = new DisplayMetrics();
107 
108     Bootstrap.applyQualifiers(config.qualifiers(), sdkConfig.getApiLevel(), configuration,
109         displayMetrics);
110 
111     Locale locale = sdkConfig.getApiLevel() >= VERSION_CODES.N
112         ? configuration.getLocales().get(0)
113         : configuration.locale;
114     Locale.setDefault(locale);
115 
116     // Looper needs to be prepared before the activity thread is created
117     if (Looper.myLooper() == null) {
118       Looper.prepareMainLooper();
119     }
120     ShadowLooper.getShadowMainLooper().resetScheduler();
121     ActivityThread activityThread = ReflectionHelpers.newInstance(ActivityThread.class);
122     RuntimeEnvironment.setActivityThread(activityThread);
123 
124     PackageParser.Package parsedPackage;
125     if (RuntimeEnvironment.useLegacyResources()) {
126       injectResourceStuffForLegacy(apkLoader, appManifest, sdkEnvironment);
127 
128       if (appManifest.getAndroidManifestFile() != null
129           && appManifest.getAndroidManifestFile().exists()) {
130         parsedPackage = LegacyManifestParser.createPackage(appManifest);
131       } else {
132         parsedPackage = new PackageParser.Package("org.robolectric.default");
133         parsedPackage.applicationInfo.targetSdkVersion = appManifest.getTargetSdkVersion();
134       }
135       // Support overriding the package name specified in the Manifest.
136       if (!Config.DEFAULT_PACKAGE_NAME.equals(config.packageName())) {
137         parsedPackage.packageName = config.packageName();
138         parsedPackage.applicationInfo.packageName = config.packageName();
139       } else {
140         parsedPackage.packageName = appManifest.getPackageName();
141         parsedPackage.applicationInfo.packageName = appManifest.getPackageName();
142       }
143     } else {
144       RuntimeEnvironment.compileTimeSystemResourcesFile =
145           apkLoader.getCompileTimeSystemResourcesFile(sdkEnvironment);
146 
147       RuntimeEnvironment.setAndroidFrameworkJarPath(
148           apkLoader.getArtifactUrl(sdkConfig.getAndroidSdkDependency()).getFile());
149 
150       // PackageParser.parseBaseApkCommon() relies on an initial Application object in order to
151       // resolve PermissionManager service.
152       createInitialApplication(appManifest, config);
153 
154       FsFile packageFile = appManifest.getApkFile();
155       parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
156 
157       // Create a new ActivityThread for RuntimeEnvironment since the original one is bound to a
158       // system context stub in createInitialApplication(), which causes Resources not found.
159       activityThread = ReflectionHelpers.newInstance(ActivityThread.class);
160       RuntimeEnvironment.setActivityThread(activityThread);
161     }
162 
163     ApplicationInfo applicationInfo = parsedPackage.applicationInfo;
164 
165     // unclear why, but prior to P the processName wasn't set
166     if (sdkConfig.getApiLevel() < P && applicationInfo.processName == null) {
167       applicationInfo.processName = parsedPackage.packageName;
168     }
169 
170     setUpPackageStorage(applicationInfo, parsedPackage);
171 
172     // Bit of a hack... Context.createPackageContext() is called before the application is created.
173     // It calls through
174     // to ActivityThread for the package which in turn calls the PackageManagerService directly.
175     // This works for now
176     // but it might be nicer to have ShadowPackageManager implementation move into the service as
177     // there is also lots of
178     // code in there that can be reusable, e.g: the XxxxIntentResolver code.
179     ShadowActivityThread.setApplicationInfo(applicationInfo);
180 
181     Class<?> contextImplClass =
182         ReflectionHelpers.loadClass(
183             getClass().getClassLoader(), ShadowContextImpl.CLASS_NAME);
184 
185     if (sdkConfig.getApiLevel() < Build.VERSION_CODES.S) {
186       ReflectionHelpers.setField(activityThread, "mCompatConfiguration", configuration);
187     } else {
188       Class<?> activityThreadInternalClass =
189               ReflectionHelpers.loadClass(
190                       getClass().getClassLoader(), "android.app.ActivityThreadInternal");
191       Class<?> configurationControllerClass =
192               ReflectionHelpers.loadClass(
193                       getClass().getClassLoader(), "android.app.ConfigurationController");
194       Object configController = ReflectionHelpers.callConstructor(configurationControllerClass,
195               from(activityThreadInternalClass, activityThread));
196       ReflectionHelpers.callInstanceMethod(configController, "setCompatConfiguration",
197               from(Configuration.class, configuration));
198       configuration = ReflectionHelpers.callInstanceMethod(configController,
199               "getCompatConfiguration");
200       ReflectionHelpers.setField(activityThread, "mConfigurationController", configController);
201     }
202     ReflectionHelpers
203         .setStaticField(ActivityThread.class, "sMainThreadHandler", new Handler(Looper.myLooper()));
204 
205     Bootstrap.setUpDisplay(configuration, displayMetrics);
206     activityThread.applyConfigurationToResources(configuration);
207 
208     Resources systemResources = Resources.getSystem();
209     systemResources.updateConfiguration(configuration, displayMetrics);
210 
211     Context systemContextImpl = ReflectionHelpers.callStaticMethod(contextImplClass,
212         "createSystemContext", from(ActivityThread.class, activityThread));
213     RuntimeEnvironment.systemContext = systemContextImpl;
214 
215     Application application = createApplication(appManifest, config);
216     RuntimeEnvironment.application = application;
217 
218     Instrumentation instrumentation =
219         createInstrumentation(activityThread, applicationInfo, application);
220 
221     if (application != null) {
222       final Class<?> appBindDataClass;
223       try {
224         appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
225       } catch (ClassNotFoundException e) {
226         throw new RuntimeException(e);
227       }
228       Object data = ReflectionHelpers.newInstance(appBindDataClass);
229       ReflectionHelpers.setField(data, "processName", "org.robolectric");
230       ReflectionHelpers.setField(data, "appInfo", applicationInfo);
231       ReflectionHelpers.setField(activityThread, "mBoundApplication", data);
232 
233       LoadedApk loadedApk = activityThread
234           .getPackageInfo(applicationInfo, null, Context.CONTEXT_INCLUDE_CODE);
235 
236       try {
237         Context contextImpl = systemContextImpl
238             .createPackageContext(applicationInfo.packageName, Context.CONTEXT_INCLUDE_CODE);
239 
240         ShadowPackageManager shadowPackageManager = Shadow.extract(contextImpl.getPackageManager());
241         shadowPackageManager.addPackageInternal(parsedPackage);
242         ReflectionHelpers
243             .setField(ActivityThread.class, activityThread, "mInitialApplication", application);
244         ShadowApplication shadowApplication = Shadow.extract(application);
245         shadowApplication.callAttach(contextImpl);
246         ReflectionHelpers.callInstanceMethod(
247             contextImpl,
248             "setOuterContext",
249             from(Context.class, application));
250       } catch (PackageManager.NameNotFoundException e) {
251         throw new RuntimeException(e);
252       }
253 
254       Secure.setLocationProviderEnabled(application.getContentResolver(), GPS_PROVIDER, true);
255 
256       Resources appResources = application.getResources();
257       ReflectionHelpers.setField(loadedApk, "mResources", appResources);
258       ReflectionHelpers.setField(loadedApk, "mApplication", application);
259 
260       registerBroadcastReceivers(application, appManifest);
261 
262       appResources.updateConfiguration(configuration, displayMetrics);
263 
264       if (ShadowAssetManager.useLegacy()) {
265         populateAssetPaths(appResources.getAssets(), appManifest);
266       }
267 
268       instrumentation.onCreate(new Bundle());
269 
270       PerfStatsCollector.getInstance()
271           .measure("application onCreate()", () -> application.onCreate());
272     }
273   }
274 
createInitialApplication(AndroidManifest appManifest, Config config)275   private void createInitialApplication(AndroidManifest appManifest, Config config) {
276     ShadowActivityThread.setApplicationInfo(createApplicationInfo(appManifest));
277     ActivityThread activityThread = ActivityThread.currentActivityThread();
278 
279     Class<?> contextImplClass =
280         ReflectionHelpers.loadClass(
281             getClass().getClassLoader(), ShadowContextImpl.CLASS_NAME);
282     Context systemContextImpl = ReflectionHelpers.callStaticMethod(contextImplClass,
283         "createSystemContext", from(ActivityThread.class, activityThread));
284 
285     Application application = createApplication(appManifest, config);
286 
287     if (application != null) {
288       try {
289         Context contextImpl = systemContextImpl
290             .createPackageContext(appManifest.getPackageName(), Context.CONTEXT_INCLUDE_CODE);
291 
292         ReflectionHelpers
293             .setField(ActivityThread.class, activityThread, "mInitialApplication", application);
294         ShadowApplication shadowApplication = Shadow.extract(application);
295         shadowApplication.callAttach(contextImpl);
296       } catch (PackageManager.NameNotFoundException e) {
297         throw new RuntimeException(e);
298       }
299     }
300   }
301 
createApplicationInfo(AndroidManifest appManifest)302   private ApplicationInfo createApplicationInfo(AndroidManifest appManifest) {
303     final ApplicationInfo info = new ApplicationInfo();
304     info.targetSdkVersion = appManifest.getTargetSdkVersion();
305     info.packageName = appManifest.getPackageName();
306     info.processName = appManifest.getProcessName();
307     return info;
308   }
309 
injectResourceStuffForLegacy(ApkLoader apkLoader, AndroidManifest appManifest, SdkEnvironment sdkEnvironment)310   private void injectResourceStuffForLegacy(ApkLoader apkLoader, AndroidManifest appManifest,
311       SdkEnvironment sdkEnvironment) {
312     PackageResourceTable systemResourceTable = apkLoader.getSystemResourceTable(sdkEnvironment);
313     PackageResourceTable appResourceTable = apkLoader.getAppResourceTable(appManifest);
314     RoutingResourceTable combinedAppResourceTable = new RoutingResourceTable(appResourceTable,
315         systemResourceTable);
316 
317     PackageResourceTable compileTimeSdkResourceTable = apkLoader.getCompileTimeSdkResourceTable();
318     ResourceTable combinedCompileTimeResourceTable =
319         new RoutingResourceTable(appResourceTable, compileTimeSdkResourceTable);
320 
321     RuntimeEnvironment.setCompileTimeResourceTable(combinedCompileTimeResourceTable);
322     RuntimeEnvironment.setAppResourceTable(combinedAppResourceTable);
323     RuntimeEnvironment.setSystemResourceTable(new RoutingResourceTable(systemResourceTable));
324 
325     try {
326       appManifest.initMetaData(combinedAppResourceTable);
327     } catch (RoboNotFoundException e1) {
328       throw new Resources.NotFoundException(e1.getMessage());
329     }
330   }
331 
populateAssetPaths(AssetManager assetManager, AndroidManifest appManifest)332   private void populateAssetPaths(AssetManager assetManager, AndroidManifest appManifest) {
333     for (AndroidManifest manifest : appManifest.getAllManifests()) {
334       if (manifest.getAssetsDirectory() != null) {
335         assetManager.addAssetPath(manifest.getAssetsDirectory().getPath());
336       }
337     }
338   }
339 
340   @VisibleForTesting
createApplication(AndroidManifest appManifest, Config config)341   static Application createApplication(AndroidManifest appManifest, Config config) {
342     Application application = null;
343     if (config != null && !Config.Builder.isDefaultApplication(config.application())) {
344       if (config.application().getCanonicalName() != null) {
345         Class<? extends Application> applicationClass;
346         try {
347           applicationClass = ClassNameResolver.resolve(null, config.application().getName());
348         } catch (ClassNotFoundException e) {
349           throw new RuntimeException(e);
350         }
351         application = ReflectionHelpers.callConstructor(applicationClass);
352       }
353     } else if (appManifest != null && appManifest.getApplicationName() != null) {
354       Class<? extends Application> applicationClass = null;
355       try {
356         applicationClass = ClassNameResolver.resolve(appManifest.getPackageName(),
357             getTestApplicationName(appManifest.getApplicationName()));
358       } catch (ClassNotFoundException e) {
359         // no problem
360       }
361 
362       if (applicationClass == null) {
363         try {
364           applicationClass = ClassNameResolver.resolve(appManifest.getPackageName(),
365               appManifest.getApplicationName());
366         } catch (ClassNotFoundException e) {
367           throw new RuntimeException(e);
368         }
369       }
370 
371       application = ReflectionHelpers.callConstructor(applicationClass);
372     } else {
373       application = new Application();
374     }
375 
376     return application;
377   }
378 
379   @VisibleForTesting
getTestApplicationName(String applicationName)380   static String getTestApplicationName(String applicationName) {
381     int lastDot = applicationName.lastIndexOf('.');
382     if (lastDot > -1) {
383       return applicationName.substring(0, lastDot) + ".Test" + applicationName.substring(lastDot + 1);
384     } else {
385       return "Test" + applicationName;
386     }
387   }
388 
createInstrumentation( ActivityThread activityThread, ApplicationInfo applicationInfo, Application application)389   private static Instrumentation createInstrumentation(
390       ActivityThread activityThread,
391       ApplicationInfo applicationInfo, Application application) {
392     Instrumentation androidInstrumentation = new RoboMonitoringInstrumentation();
393     ReflectionHelpers.setField(activityThread, "mInstrumentation", androidInstrumentation);
394 
395     final ComponentName component =
396         new ComponentName(
397             applicationInfo.packageName, androidInstrumentation.getClass().getSimpleName());
398     if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN_MR1) {
399       ReflectionHelpers.callInstanceMethod(androidInstrumentation, "init",
400           from(ActivityThread.class, activityThread),
401           from(Context.class, application),
402           from(Context.class, application),
403           from(ComponentName.class, component),
404           from(IInstrumentationWatcher.class, null));
405     } else {
406       ReflectionHelpers.callInstanceMethod(androidInstrumentation,
407           "init",
408           from(ActivityThread.class, activityThread),
409           from(Context.class, application),
410           from(Context.class, application),
411           from(ComponentName.class, component),
412           from(IInstrumentationWatcher.class, null),
413           from(IUiAutomationConnection.class, null));
414     }
415 
416     return androidInstrumentation;
417   }
418 
419   /**
420    * Create a file system safe directory path name for the current test.
421    */
createTestDataDirRootPath(Method method)422   private String createTestDataDirRootPath(Method method) {
423     return method.getClass().getSimpleName() + "_" + method.getName().replaceAll("[^a-zA-Z0-9.-]", "_");
424   }
425 
426   @Override
getMainThread()427   public Thread getMainThread() {
428     return RuntimeEnvironment.getMainThread();
429   }
430 
431   @Override
setMainThread(Thread newMainThread)432   public void setMainThread(Thread newMainThread) {
433     RuntimeEnvironment.setMainThread(newMainThread);
434   }
435 
436   @Override
tearDownApplication()437   public void tearDownApplication() {
438     if (RuntimeEnvironment.application != null) {
439       RuntimeEnvironment.application.onTerminate();
440     }
441   }
442 
443   @Override
getCurrentApplication()444   public Object getCurrentApplication() {
445     return RuntimeEnvironment.application;
446   }
447 
448   // TODO(christianw): reconcile with ShadowPackageManager.setUpPackageStorage
setUpPackageStorage(ApplicationInfo applicationInfo, PackageParser.Package parsedPackage)449   private void setUpPackageStorage(ApplicationInfo applicationInfo,
450       PackageParser.Package parsedPackage) {
451     // TempDirectory tempDirectory = RuntimeEnvironment.getTempDirectory();
452     // packageInfo.setVolumeUuid(tempDirectory.createIfNotExists(packageInfo.packageName +
453     // "-dataDir").toAbsolutePath().toString());
454 
455     if (RuntimeEnvironment.useLegacyResources()) {
456       applicationInfo.sourceDir =
457           createTempDir(applicationInfo.packageName + "-sourceDir");
458       applicationInfo.publicSourceDir =
459           createTempDir(applicationInfo.packageName + "-publicSourceDir");
460     } else {
461       if (sdkConfig.getApiLevel() <= VERSION_CODES.KITKAT) {
462         String sourcePath = ReflectionHelpers.getField(parsedPackage, "mPath");
463         if (sourcePath == null) {
464           sourcePath = createTempDir("sourceDir");
465         }
466         applicationInfo.publicSourceDir = sourcePath;
467         applicationInfo.sourceDir = sourcePath;
468       } else {
469         applicationInfo.publicSourceDir = parsedPackage.codePath;
470         applicationInfo.sourceDir = parsedPackage.codePath;
471       }
472     }
473 
474     applicationInfo.dataDir = createTempDir(applicationInfo.packageName + "-dataDir");
475 
476     if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.N) {
477       applicationInfo.credentialProtectedDataDir = createTempDir("userDataDir");
478       applicationInfo.deviceProtectedDataDir = createTempDir("deviceDataDir");
479     }
480   }
481 
createTempDir(String name)482   private String createTempDir(String name) {
483     return RuntimeEnvironment.getTempDirectory()
484         .createIfNotExists(name)
485         .toAbsolutePath()
486         .toString();
487   }
488 
489   // TODO move/replace this with packageManager
490   @VisibleForTesting
registerBroadcastReceivers( Application application, AndroidManifest androidManifest)491   static void registerBroadcastReceivers(
492       Application application, AndroidManifest androidManifest) {
493     for (BroadcastReceiverData receiver : androidManifest.getBroadcastReceivers()) {
494       IntentFilter filter = new IntentFilter();
495       for (String action : receiver.getActions()) {
496         filter.addAction(action);
497       }
498       String receiverClassName = replaceLastDotWith$IfInnerStaticClass(receiver.getName());
499       application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
500     }
501   }
502 
replaceLastDotWith$IfInnerStaticClass(String receiverClassName)503   private static String replaceLastDotWith$IfInnerStaticClass(String receiverClassName) {
504     String[] splits = receiverClassName.split("\\.", 0);
505     String staticInnerClassRegex = "[A-Z][a-zA-Z]*";
506     if (splits.length > 1
507         && splits[splits.length - 1].matches(staticInnerClassRegex)
508         && splits[splits.length - 2].matches(staticInnerClassRegex)) {
509       int lastDotIndex = receiverClassName.lastIndexOf(".");
510       StringBuilder buffer = new StringBuilder(receiverClassName);
511       buffer.setCharAt(lastDotIndex, '$');
512       return buffer.toString();
513     }
514     return receiverClassName;
515   }
516 }
517