package com.xtremelabs.robolectric.shadows; import android.app.Application; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.res.Resources; import android.os.IBinder; import android.os.Looper; import android.view.LayoutInflater; import android.widget.Toast; import com.xtremelabs.robolectric.Robolectric; import com.xtremelabs.robolectric.internal.Implementation; import com.xtremelabs.robolectric.internal.Implements; import com.xtremelabs.robolectric.internal.RealObject; import com.xtremelabs.robolectric.res.ResourceLoader; import com.xtremelabs.robolectric.tester.org.apache.http.FakeHttpLayer; import com.xtremelabs.robolectric.util.Scheduler; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import static com.xtremelabs.robolectric.Robolectric.newInstanceOf; import static com.xtremelabs.robolectric.Robolectric.shadowOf; /** * Shadows the {@code android.app.Application} class. */ @SuppressWarnings({"UnusedDeclaration"}) @Implements(Application.class) public class ShadowApplication extends ShadowContextWrapper { private static final Map SYSTEM_SERVICE_MAP = new HashMap(); static { // note that these are different! // They specify concrete classes within Robolectric for interfaces or abstract classes defined by Android SYSTEM_SERVICE_MAP.put(Context.WINDOW_SERVICE, "com.xtremelabs.robolectric.tester.android.view.TestWindowManager"); SYSTEM_SERVICE_MAP.put(Context.CLIPBOARD_SERVICE, "com.xtremelabs.robolectric.tester.android.text.TestClipboardManager"); SYSTEM_SERVICE_MAP.put(Context.SENSOR_SERVICE, "android.hardware.TestSensorManager"); SYSTEM_SERVICE_MAP.put(Context.VIBRATOR_SERVICE, "android.os.TestVibrator"); // the rest are as mapped in docs... SYSTEM_SERVICE_MAP.put(Context.LAYOUT_INFLATER_SERVICE, "android.view.LayoutInflater"); SYSTEM_SERVICE_MAP.put(Context.ACTIVITY_SERVICE, "android.app.ActivityManager"); SYSTEM_SERVICE_MAP.put(Context.POWER_SERVICE, "android.os.PowerManager"); SYSTEM_SERVICE_MAP.put(Context.ALARM_SERVICE, "android.app.AlarmManager"); SYSTEM_SERVICE_MAP.put(Context.NOTIFICATION_SERVICE, "android.app.NotificationManager"); SYSTEM_SERVICE_MAP.put(Context.KEYGUARD_SERVICE, "android.app.KeyguardManager"); SYSTEM_SERVICE_MAP.put(Context.LOCATION_SERVICE, "android.location.LocationManager"); SYSTEM_SERVICE_MAP.put(Context.SEARCH_SERVICE, "android.app.SearchManager"); SYSTEM_SERVICE_MAP.put(Context.STORAGE_SERVICE, "android.os.storage.StorageManager"); SYSTEM_SERVICE_MAP.put(Context.CONNECTIVITY_SERVICE, "android.net.ConnectivityManager"); SYSTEM_SERVICE_MAP.put(Context.WIFI_SERVICE, "android.net.wifi.WifiManager"); SYSTEM_SERVICE_MAP.put(Context.AUDIO_SERVICE, "android.media.AudioManager"); SYSTEM_SERVICE_MAP.put(Context.TELEPHONY_SERVICE, "android.telephony.TelephonyManager"); SYSTEM_SERVICE_MAP.put(Context.INPUT_METHOD_SERVICE, "android.view.inputmethod.InputMethodManager"); SYSTEM_SERVICE_MAP.put(Context.UI_MODE_SERVICE, "android.app.UiModeManager"); SYSTEM_SERVICE_MAP.put(Context.DOWNLOAD_SERVICE, "android.app.DownloadManager"); } @RealObject private Application realApplication; private ResourceLoader resourceLoader; private ContentResolver contentResolver; private Map systemServices = new HashMap(); private List startedActivities = new ArrayList(); private List startedServices = new ArrayList(); private List stoppedServies = new ArrayList(); private List broadcastIntents = new ArrayList(); private List unboundServiceConnections = new ArrayList(); private List registeredReceivers = new ArrayList(); private Map stickyIntents = new HashMap(); private FakeHttpLayer fakeHttpLayer = new FakeHttpLayer(); private Looper mainLooper = ShadowLooper.myLooper(); private Scheduler backgroundScheduler = new Scheduler(); private Map> sharedPreferenceMap = new HashMap>(); private ArrayList shownToasts = new ArrayList(); private ShadowAlertDialog latestAlertDialog; private ShadowDialog latestDialog; private Object bluetoothAdapter = Robolectric.newInstanceOf("android.bluetooth.BluetoothAdapter"); private Resources resources; // these are managed by the AppSingletonizier... kinda gross, sorry [xw] LayoutInflater layoutInflater; AppWidgetManager appWidgetManager; private ServiceConnection serviceConnection; private ComponentName componentNameForBindService; private IBinder serviceForBindService; private List unbindableActions = new ArrayList(); /** * Associates a {@code ResourceLoader} with an {@code Application} instance * * @param application application * @param resourceLoader resource loader * @return the application * todo: make this non-static? */ public static Application bind(Application application, ResourceLoader resourceLoader) { ShadowApplication shadowApplication = shadowOf(application); if (shadowApplication.resourceLoader != null) throw new RuntimeException("ResourceLoader already set!"); shadowApplication.resourceLoader = resourceLoader; shadowApplication.resources = ShadowResources.bind(new Resources(null, null, null), resourceLoader); return application; } public List getShownToasts() { return shownToasts; } public Scheduler getBackgroundScheduler() { return backgroundScheduler; } @Override @Implementation public Context getApplicationContext() { return realApplication; } @Override @Implementation public Resources getResources() { if (resources == null ) { resources = ShadowResources.bind(new Resources(null, null, null), resourceLoader); } return resources; } @Implementation @Override public ContentResolver getContentResolver() { if (contentResolver == null) { contentResolver = new ContentResolver(realApplication) { }; } return contentResolver; } @Implementation @Override public Object getSystemService(String name) { if (name.equals(Context.LAYOUT_INFLATER_SERVICE)) { return LayoutInflater.from(realApplication); } else { Object service = systemServices.get(name); if (service == null) { String serviceClassName = SYSTEM_SERVICE_MAP.get(name); if (serviceClassName != null) { try { service = newInstanceOf(Class.forName(serviceClassName)); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } systemServices.put(name, service); } } return service; } } @Implementation @Override public void startActivity(Intent intent) { startedActivities.add(intent); } @Implementation @Override public ComponentName startService(Intent intent) { startedServices.add(intent); return new ComponentName("some.service.package", "SomeServiceName-FIXME"); } @Implementation @Override public boolean stopService(Intent name) { stoppedServies.add(name); return startedServices.contains(name); } public void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) { this.componentNameForBindService = name; this.serviceForBindService = service; } @Implementation public boolean bindService(Intent intent, final ServiceConnection serviceConnection, int i) { if (unbindableActions.contains(intent.getAction())) { return false; } startedServices.add(intent); shadowOf(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { serviceConnection.onServiceConnected(componentNameForBindService, serviceForBindService); } }, 0); return true; } @Implementation public void unbindService(final ServiceConnection serviceConnection) { unboundServiceConnections.add(serviceConnection); shadowOf(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { serviceConnection.onServiceDisconnected(componentNameForBindService); } }, 0); } public List getUnboundServiceConnections() { return unboundServiceConnections; } /** * Consumes the most recent {@code Intent} started by {@link #startActivity(android.content.Intent)} and returns it. * * @return the most recently started {@code Intent} */ @Override public Intent getNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return startedActivities.remove(0); } } /** * Returns the most recent {@code Intent} started by {@link #startActivity(android.content.Intent)} without * consuming it. * * @return the most recently started {@code Intent} */ @Override public Intent peekNextStartedActivity() { if (startedActivities.isEmpty()) { return null; } else { return startedActivities.get(0); } } /** * Consumes the most recent {@code Intent} started by {@link #startService(android.content.Intent)} and returns it. * * @return the most recently started {@code Intent} */ @Override public Intent getNextStartedService() { if (startedServices.isEmpty()) { return null; } else { return startedServices.remove(0); } } /** * Returns the most recent {@code Intent} started by {@link #startService(android.content.Intent)} without * consuming it. * * @return the most recently started {@code Intent} */ @Override public Intent peekNextStartedService() { if (startedServices.isEmpty()) { return null; } else { return startedServices.get(0); } } /** * Clears all {@code Intent} started by {@link #startService(android.content.Intent)} */ @Override public void clearStartedServices() { startedServices.clear(); } /** * Consumes the {@code Intent} requested to stop a service by {@link #stopService(android.content.Intent)} * from the bottom of the stack of stop requests. */ @Override public Intent getNextStoppedService() { if (stoppedServies.isEmpty()) { return null; } else { return stoppedServies.remove(0); } } /** * Non-Android accessor (and a handy way to get a working {@code ResourceLoader} * * @return the {@code ResourceLoader} associated with this Application */ public ResourceLoader getResourceLoader() { return resourceLoader; } /** * Broadcasts the {@code Intent} by iterating through the registered receivers, invoking their filters, and calling * {@code onRecieve(Application, Intent)} as appropriate. Does not enqueue the {@code Intent} for later inspection. * * @param intent the {@code Intent} to broadcast * todo: enqueue the Intent for later inspection */ @Override @Implementation public void sendBroadcast(Intent intent) { broadcastIntents.add(intent); List copy = new ArrayList(); copy.addAll(registeredReceivers); for (Wrapper wrapper : copy) { if (wrapper.intentFilter.matchAction(intent.getAction())) { wrapper.broadcastReceiver.onReceive(realApplication, intent); } } } public List getBroadcastIntents() { return broadcastIntents; } @Implementation public void sendStickyBroadcast(Intent intent) { stickyIntents.put(intent.getAction(), intent); sendBroadcast(intent); } /** * Always returns {@code null} * * @return {@code null} */ @Override @Implementation public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { return registerReceiverWithContext(receiver, filter, realApplication); } Intent registerReceiverWithContext(BroadcastReceiver receiver, IntentFilter filter, Context context) { if (receiver != null) { registeredReceivers.add(new Wrapper(receiver, filter, context)); } return getStickyIntent(filter); } private Intent getStickyIntent(IntentFilter filter) { for (Intent stickyIntent : stickyIntents.values()) { String action = null; for (int i = 0; i < filter.countActions(); i++) { action = filter.getAction(i); if (stickyIntent.getAction().equals(action)) { return stickyIntent; } } } return null; } @Override @Implementation public void unregisterReceiver(BroadcastReceiver broadcastReceiver) { boolean found = false; Iterator iterator = registeredReceivers.iterator(); while (iterator.hasNext()) { Wrapper wrapper = iterator.next(); if (wrapper.broadcastReceiver == broadcastReceiver) { iterator.remove(); found = true; } } if (!found) { throw new IllegalArgumentException("Receiver not registered: " + broadcastReceiver); } } /** * Iterates through all of the registered receivers on this {@code Application} and if any of them match the given * {@code Context} object throws a {@code RuntimeException} * * @param context the {@code Context} to check for on each of the remaining registered receivers * @param type the type to report for the context if an exception is thrown * @throws RuntimeException if there are any recievers registered with the given {@code Context} */ public void assertNoBroadcastListenersRegistered(Context context, String type) { for (Wrapper registeredReceiver : registeredReceivers) { if (registeredReceiver.context == context) { RuntimeException e = new IllegalStateException(type + " " + context + " leaked has leaked IntentReceiver " + registeredReceiver.broadcastReceiver + " that was originally registered here. " + "Are you missing a call to unregisterReceiver()?"); e.setStackTrace(registeredReceiver.exception.getStackTrace()); throw e; } } } public void assertNoBroadcastListenersOfActionRegistered(Context context, String action) { for (Wrapper registeredReceiver : registeredReceivers) { if (registeredReceiver.context == context) { Iterator actions = registeredReceiver.intentFilter.actionsIterator(); while (actions.hasNext()) { if (actions.next().equals(action)) { RuntimeException e = new IllegalStateException("Unexpected BroadcastReceiver on " + context + " with action " + action + " " + registeredReceiver.broadcastReceiver + " that was originally registered here:"); e.setStackTrace(registeredReceiver.exception.getStackTrace()); throw e; } } } } } public boolean hasReceiverForIntent(Intent intent) { for (Wrapper wrapper : registeredReceivers) { if (wrapper.intentFilter.matchAction(intent.getAction())) { return true; } } return false; } public List getReceiversForIntent(Intent intent) { ArrayList broadcastReceivers = new ArrayList(); for (Wrapper wrapper : registeredReceivers) { if (wrapper.intentFilter.matchAction(intent.getAction())) { broadcastReceivers.add(wrapper.getBroadcastReceiver()); } } return broadcastReceivers; } /** * Non-Android accessor. * * @return list of {@link Wrapper}s for registered receivers */ public List getRegisteredReceivers() { return registeredReceivers; } /** * Non-Android accessor. * * @return the layout inflater used by this {@code Application} */ public LayoutInflater getLayoutInflater() { return layoutInflater; } /** * Non-Android accessor. * * @return the app widget manager used by this {@code Application} */ public AppWidgetManager getAppWidgetManager() { return appWidgetManager; } public FakeHttpLayer getFakeHttpLayer() { return fakeHttpLayer; } @Override @Implementation public Looper getMainLooper() { return mainLooper; } public Map> getSharedPreferenceMap() { return sharedPreferenceMap; } public ShadowAlertDialog getLatestAlertDialog() { return latestAlertDialog; } public void setLatestAlertDialog(ShadowAlertDialog latestAlertDialog) { this.latestAlertDialog = latestAlertDialog; } public ShadowDialog getLatestDialog() { return latestDialog; } public void setLatestDialog(ShadowDialog latestDialog) { this.latestDialog = latestDialog; } public Object getBluetoothAdapter() { return bluetoothAdapter; } public void declareActionUnbindable(String action) { unbindableActions.add(action); } public void setSystemService(String key, Object service) { systemServices.put(key, service); } public class Wrapper { public BroadcastReceiver broadcastReceiver; public IntentFilter intentFilter; public Context context; public Throwable exception; public Wrapper(BroadcastReceiver broadcastReceiver, IntentFilter intentFilter, Context context) { this.broadcastReceiver = broadcastReceiver; this.intentFilter = intentFilter; this.context = context; exception = new Throwable(); } public BroadcastReceiver getBroadcastReceiver() { return broadcastReceiver; } public IntentFilter getIntentFilter() { return intentFilter; } public Context getContext() { return context; } } }