1package com.xtremelabs.robolectric.shadows;
2
3import android.app.Application;
4import android.appwidget.AppWidgetManager;
5import android.content.BroadcastReceiver;
6import android.content.ComponentName;
7import android.content.ContentResolver;
8import android.content.Context;
9import android.content.Intent;
10import android.content.IntentFilter;
11import android.content.ServiceConnection;
12import android.content.res.Resources;
13import android.os.IBinder;
14import android.os.Looper;
15import android.view.LayoutInflater;
16import android.widget.Toast;
17import com.xtremelabs.robolectric.Robolectric;
18import com.xtremelabs.robolectric.internal.Implementation;
19import com.xtremelabs.robolectric.internal.Implements;
20import com.xtremelabs.robolectric.internal.RealObject;
21import com.xtremelabs.robolectric.res.ResourceLoader;
22import com.xtremelabs.robolectric.tester.org.apache.http.FakeHttpLayer;
23import com.xtremelabs.robolectric.util.Scheduler;
24
25import java.util.ArrayList;
26import java.util.HashMap;
27import java.util.Iterator;
28import java.util.List;
29import java.util.Map;
30
31import static com.xtremelabs.robolectric.Robolectric.newInstanceOf;
32import static com.xtremelabs.robolectric.Robolectric.shadowOf;
33
34/**
35 * Shadows the {@code android.app.Application} class.
36 */
37@SuppressWarnings({"UnusedDeclaration"})
38@Implements(Application.class)
39public class ShadowApplication extends ShadowContextWrapper {
40    private static final Map<String, String> SYSTEM_SERVICE_MAP = new HashMap<String, String>();
41
42    static {
43        // note that these are different!
44    	// They specify concrete classes within Robolectric for interfaces or abstract classes defined by Android
45        SYSTEM_SERVICE_MAP.put(Context.WINDOW_SERVICE, "com.xtremelabs.robolectric.tester.android.view.TestWindowManager");
46        SYSTEM_SERVICE_MAP.put(Context.CLIPBOARD_SERVICE, "com.xtremelabs.robolectric.tester.android.text.TestClipboardManager");
47        SYSTEM_SERVICE_MAP.put(Context.SENSOR_SERVICE, "android.hardware.TestSensorManager");
48        SYSTEM_SERVICE_MAP.put(Context.VIBRATOR_SERVICE, "android.os.TestVibrator");
49
50        // the rest are as mapped in docs...
51        SYSTEM_SERVICE_MAP.put(Context.LAYOUT_INFLATER_SERVICE, "android.view.LayoutInflater");
52        SYSTEM_SERVICE_MAP.put(Context.ACTIVITY_SERVICE, "android.app.ActivityManager");
53        SYSTEM_SERVICE_MAP.put(Context.POWER_SERVICE, "android.os.PowerManager");
54        SYSTEM_SERVICE_MAP.put(Context.ALARM_SERVICE, "android.app.AlarmManager");
55        SYSTEM_SERVICE_MAP.put(Context.NOTIFICATION_SERVICE, "android.app.NotificationManager");
56        SYSTEM_SERVICE_MAP.put(Context.KEYGUARD_SERVICE, "android.app.KeyguardManager");
57        SYSTEM_SERVICE_MAP.put(Context.LOCATION_SERVICE, "android.location.LocationManager");
58        SYSTEM_SERVICE_MAP.put(Context.SEARCH_SERVICE, "android.app.SearchManager");
59        SYSTEM_SERVICE_MAP.put(Context.STORAGE_SERVICE, "android.os.storage.StorageManager");
60        SYSTEM_SERVICE_MAP.put(Context.CONNECTIVITY_SERVICE, "android.net.ConnectivityManager");
61        SYSTEM_SERVICE_MAP.put(Context.WIFI_SERVICE, "android.net.wifi.WifiManager");
62        SYSTEM_SERVICE_MAP.put(Context.AUDIO_SERVICE, "android.media.AudioManager");
63        SYSTEM_SERVICE_MAP.put(Context.TELEPHONY_SERVICE, "android.telephony.TelephonyManager");
64        SYSTEM_SERVICE_MAP.put(Context.INPUT_METHOD_SERVICE, "android.view.inputmethod.InputMethodManager");
65        SYSTEM_SERVICE_MAP.put(Context.UI_MODE_SERVICE, "android.app.UiModeManager");
66        SYSTEM_SERVICE_MAP.put(Context.DOWNLOAD_SERVICE, "android.app.DownloadManager");
67    }
68
69    @RealObject private Application realApplication;
70
71    private ResourceLoader resourceLoader;
72    private ContentResolver contentResolver;
73    private Map<String, Object> systemServices = new HashMap<String, Object>();
74    private List<Intent> startedActivities = new ArrayList<Intent>();
75    private List<Intent> startedServices = new ArrayList<Intent>();
76    private List<Intent> stoppedServies = new ArrayList<Intent>();
77    private List<Intent> broadcastIntents = new ArrayList<Intent>();
78    private List<ServiceConnection> unboundServiceConnections = new ArrayList<ServiceConnection>();
79    private List<Wrapper> registeredReceivers = new ArrayList<Wrapper>();
80    private Map<String, Intent> stickyIntents = new HashMap<String, Intent>();
81    private FakeHttpLayer fakeHttpLayer = new FakeHttpLayer();
82    private Looper mainLooper = ShadowLooper.myLooper();
83    private Scheduler backgroundScheduler = new Scheduler();
84    private Map<String, Map<String, Object>> sharedPreferenceMap = new HashMap<String, Map<String, Object>>();
85    private ArrayList<Toast> shownToasts = new ArrayList<Toast>();
86    private ShadowAlertDialog latestAlertDialog;
87    private ShadowDialog latestDialog;
88    private Object bluetoothAdapter = Robolectric.newInstanceOf("android.bluetooth.BluetoothAdapter");
89    private Resources resources;
90
91    // these are managed by the AppSingletonizier... kinda gross, sorry [xw]
92    LayoutInflater layoutInflater;
93    AppWidgetManager appWidgetManager;
94    private ServiceConnection serviceConnection;
95    private ComponentName componentNameForBindService;
96    private IBinder serviceForBindService;
97    private List<String> unbindableActions = new ArrayList<String>();
98
99    /**
100     * Associates a {@code ResourceLoader} with an {@code Application} instance
101     *
102     * @param application    application
103     * @param resourceLoader resource loader
104     * @return the application
105     *         todo: make this non-static?
106     */
107    public static Application bind(Application application, ResourceLoader resourceLoader) {
108        ShadowApplication shadowApplication = shadowOf(application);
109        if (shadowApplication.resourceLoader != null) throw new RuntimeException("ResourceLoader already set!");
110        shadowApplication.resourceLoader = resourceLoader;
111        shadowApplication.resources = ShadowResources.bind(new Resources(null, null, null), resourceLoader);
112        return application;
113    }
114
115    public List<Toast> getShownToasts() {
116        return shownToasts;
117    }
118
119    public Scheduler getBackgroundScheduler() {
120        return backgroundScheduler;
121    }
122
123    @Override
124    @Implementation
125    public Context getApplicationContext() {
126        return realApplication;
127    }
128
129    @Override
130    @Implementation
131    public Resources getResources() {
132        if (resources == null ) {
133        	resources = ShadowResources.bind(new Resources(null, null, null), resourceLoader);
134        }
135        return resources;
136    }
137
138    @Implementation
139    @Override
140    public ContentResolver getContentResolver() {
141        if (contentResolver == null) {
142            contentResolver = new ContentResolver(realApplication) {
143            };
144        }
145        return contentResolver;
146    }
147
148    @Implementation
149    @Override
150    public Object getSystemService(String name) {
151        if (name.equals(Context.LAYOUT_INFLATER_SERVICE)) {
152            return LayoutInflater.from(realApplication);
153        } else {
154            Object service = systemServices.get(name);
155            if (service == null) {
156                String serviceClassName = SYSTEM_SERVICE_MAP.get(name);
157                if (serviceClassName != null) {
158                    try {
159                        service = newInstanceOf(Class.forName(serviceClassName));
160                    } catch (ClassNotFoundException e) {
161                        throw new RuntimeException(e);
162                    }
163                    systemServices.put(name, service);
164                }
165            }
166            return service;
167        }
168    }
169
170    @Implementation
171    @Override
172    public void startActivity(Intent intent) {
173        startedActivities.add(intent);
174    }
175
176    @Implementation
177    @Override
178    public ComponentName startService(Intent intent) {
179        startedServices.add(intent);
180        return new ComponentName("some.service.package", "SomeServiceName-FIXME");
181    }
182
183    @Implementation
184    @Override
185    public boolean stopService(Intent name) {
186        stoppedServies.add(name);
187
188        return startedServices.contains(name);
189    }
190
191    public void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) {
192        this.componentNameForBindService = name;
193        this.serviceForBindService = service;
194    }
195
196    @Implementation
197    public boolean bindService(Intent intent, final ServiceConnection serviceConnection, int i) {
198        if (unbindableActions.contains(intent.getAction())) {
199            return false;
200        }
201        startedServices.add(intent);
202        shadowOf(Looper.getMainLooper()).post(new Runnable() {
203            @Override
204            public void run() {
205                serviceConnection.onServiceConnected(componentNameForBindService, serviceForBindService);
206            }
207        }, 0);
208        return true;
209    }
210
211    @Implementation
212    public void unbindService(final ServiceConnection serviceConnection) {
213        unboundServiceConnections.add(serviceConnection);
214        shadowOf(Looper.getMainLooper()).post(new Runnable() {
215            @Override
216            public void run() {
217                serviceConnection.onServiceDisconnected(componentNameForBindService);
218            }
219        }, 0);
220    }
221
222    public List<ServiceConnection> getUnboundServiceConnections() {
223        return unboundServiceConnections;
224    }
225    /**
226     * Consumes the most recent {@code Intent} started by {@link #startActivity(android.content.Intent)} and returns it.
227     *
228     * @return the most recently started {@code Intent}
229     */
230    @Override
231    public Intent getNextStartedActivity() {
232        if (startedActivities.isEmpty()) {
233            return null;
234        } else {
235            return startedActivities.remove(0);
236        }
237    }
238
239    /**
240     * Returns the most recent {@code Intent} started by {@link #startActivity(android.content.Intent)} without
241     * consuming it.
242     *
243     * @return the most recently started {@code Intent}
244     */
245    @Override
246    public Intent peekNextStartedActivity() {
247        if (startedActivities.isEmpty()) {
248            return null;
249        } else {
250            return startedActivities.get(0);
251        }
252    }
253
254    /**
255     * Consumes the most recent {@code Intent} started by {@link #startService(android.content.Intent)} and returns it.
256     *
257     * @return the most recently started {@code Intent}
258     */
259    @Override
260    public Intent getNextStartedService() {
261        if (startedServices.isEmpty()) {
262            return null;
263        } else {
264            return startedServices.remove(0);
265        }
266    }
267
268    /**
269     * Returns the most recent {@code Intent} started by {@link #startService(android.content.Intent)} without
270     * consuming it.
271     *
272     * @return the most recently started {@code Intent}
273     */
274    @Override
275    public Intent peekNextStartedService() {
276        if (startedServices.isEmpty()) {
277            return null;
278        } else {
279            return startedServices.get(0);
280        }
281    }
282
283    /**
284     * Clears all {@code Intent} started by {@link #startService(android.content.Intent)}
285     */
286    @Override
287    public void clearStartedServices() {
288        startedServices.clear();
289    }
290
291    /**
292     * Consumes the {@code Intent} requested to stop a service by {@link #stopService(android.content.Intent)}
293     * from the bottom of the stack of stop requests.
294     */
295    @Override
296    public Intent getNextStoppedService() {
297        if (stoppedServies.isEmpty()) {
298            return null;
299        } else {
300            return stoppedServies.remove(0);
301        }
302    }
303
304    /**
305     * Non-Android accessor (and a handy way to get a working {@code ResourceLoader}
306     *
307     * @return the {@code ResourceLoader} associated with this Application
308     */
309    public ResourceLoader getResourceLoader() {
310        return resourceLoader;
311    }
312
313    /**
314     * Broadcasts the {@code Intent} by iterating through the registered receivers, invoking their filters, and calling
315     * {@code onRecieve(Application, Intent)} as appropriate. Does not enqueue the {@code Intent} for later inspection.
316     *
317     * @param intent the {@code Intent} to broadcast
318     *               todo: enqueue the Intent for later inspection
319     */
320    @Override
321    @Implementation
322    public void sendBroadcast(Intent intent) {
323        broadcastIntents.add(intent);
324
325        List<Wrapper> copy = new ArrayList<Wrapper>();
326        copy.addAll(registeredReceivers);
327        for (Wrapper wrapper : copy) {
328            if (wrapper.intentFilter.matchAction(intent.getAction())) {
329                wrapper.broadcastReceiver.onReceive(realApplication, intent);
330            }
331        }
332    }
333
334    public List<Intent> getBroadcastIntents() {
335        return broadcastIntents;
336    }
337
338    @Implementation
339    public void sendStickyBroadcast(Intent intent) {
340        stickyIntents.put(intent.getAction(), intent);
341        sendBroadcast(intent);
342    }
343
344    /**
345     * Always returns {@code null}
346     *
347     * @return {@code null}
348     */
349    @Override
350    @Implementation
351    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
352        return registerReceiverWithContext(receiver, filter, realApplication);
353    }
354
355    Intent registerReceiverWithContext(BroadcastReceiver receiver, IntentFilter filter, Context context) {
356        if (receiver != null) {
357            registeredReceivers.add(new Wrapper(receiver, filter, context));
358        }
359        return getStickyIntent(filter);
360    }
361
362    private Intent getStickyIntent(IntentFilter filter) {
363        for (Intent stickyIntent : stickyIntents.values()) {
364            String action = null;
365            for (int i = 0; i < filter.countActions(); i++) {
366                action = filter.getAction(i);
367                if (stickyIntent.getAction().equals(action)) {
368                    return stickyIntent;
369                }
370            }
371        }
372
373        return null;
374    }
375
376    @Override
377    @Implementation
378    public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
379        boolean found = false;
380        Iterator<Wrapper> iterator = registeredReceivers.iterator();
381        while (iterator.hasNext()) {
382            Wrapper wrapper = iterator.next();
383            if (wrapper.broadcastReceiver == broadcastReceiver) {
384                iterator.remove();
385                found = true;
386            }
387        }
388        if (!found) {
389            throw new IllegalArgumentException("Receiver not registered: " + broadcastReceiver);
390        }
391    }
392
393    /**
394     * Iterates through all of the registered receivers on this {@code Application} and if any of them match the given
395     * {@code Context} object throws a {@code RuntimeException}
396     *
397     * @param context the {@code Context} to check for on each of the remaining registered receivers
398     * @param type    the type to report for the context if an exception is thrown
399     * @throws RuntimeException if there are any recievers registered with the given {@code Context}
400     */
401    public void assertNoBroadcastListenersRegistered(Context context, String type) {
402        for (Wrapper registeredReceiver : registeredReceivers) {
403            if (registeredReceiver.context == context) {
404                RuntimeException e = new IllegalStateException(type + " " + context + " leaked has leaked IntentReceiver "
405                        + registeredReceiver.broadcastReceiver + " that was originally registered here. " +
406                        "Are you missing a call to unregisterReceiver()?");
407                e.setStackTrace(registeredReceiver.exception.getStackTrace());
408                throw e;
409            }
410        }
411    }
412
413    public void assertNoBroadcastListenersOfActionRegistered(Context context, String action) {
414        for (Wrapper registeredReceiver : registeredReceivers) {
415            if (registeredReceiver.context == context) {
416                Iterator<String> actions = registeredReceiver.intentFilter.actionsIterator();
417                while (actions.hasNext()) {
418                    if (actions.next().equals(action)) {
419                        RuntimeException e = new IllegalStateException("Unexpected BroadcastReceiver on " + context +
420                                " with action " + action + " "
421                                + registeredReceiver.broadcastReceiver + " that was originally registered here:");
422                        e.setStackTrace(registeredReceiver.exception.getStackTrace());
423                        throw e;
424                    }
425                }
426            }
427        }
428    }
429
430    public boolean hasReceiverForIntent(Intent intent) {
431        for (Wrapper wrapper : registeredReceivers) {
432            if (wrapper.intentFilter.matchAction(intent.getAction())) {
433                return true;
434            }
435        }
436        return false;
437    }
438
439    public List<BroadcastReceiver> getReceiversForIntent(Intent intent) {
440        ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<BroadcastReceiver>();
441        for (Wrapper wrapper : registeredReceivers) {
442            if (wrapper.intentFilter.matchAction(intent.getAction())) {
443                broadcastReceivers.add(wrapper.getBroadcastReceiver());
444            }
445        }
446        return broadcastReceivers;
447    }
448
449    /**
450     * Non-Android accessor.
451     *
452     * @return list of {@link Wrapper}s for registered receivers
453     */
454    public List<Wrapper> getRegisteredReceivers() {
455        return registeredReceivers;
456    }
457
458    /**
459     * Non-Android accessor.
460     *
461     * @return the layout inflater used by this {@code Application}
462     */
463    public LayoutInflater getLayoutInflater() {
464        return layoutInflater;
465    }
466
467    /**
468     * Non-Android accessor.
469     *
470     * @return the app widget manager used by this {@code Application}
471     */
472    public AppWidgetManager getAppWidgetManager() {
473        return appWidgetManager;
474    }
475
476    public FakeHttpLayer getFakeHttpLayer() {
477        return fakeHttpLayer;
478    }
479
480    @Override
481    @Implementation
482    public Looper getMainLooper() {
483        return mainLooper;
484    }
485
486    public Map<String, Map<String, Object>> getSharedPreferenceMap() {
487        return sharedPreferenceMap;
488    }
489
490    public ShadowAlertDialog getLatestAlertDialog() {
491        return latestAlertDialog;
492    }
493
494    public void setLatestAlertDialog(ShadowAlertDialog latestAlertDialog) {
495        this.latestAlertDialog = latestAlertDialog;
496    }
497
498    public ShadowDialog getLatestDialog() {
499        return latestDialog;
500    }
501
502    public void setLatestDialog(ShadowDialog latestDialog) {
503        this.latestDialog = latestDialog;
504    }
505
506    public Object getBluetoothAdapter() {
507        return bluetoothAdapter;
508    }
509
510    public void declareActionUnbindable(String action) {
511        unbindableActions.add(action);
512    }
513
514    public void setSystemService(String key, Object service) {
515        systemServices.put(key, service);
516    }
517
518    public class Wrapper {
519        public BroadcastReceiver broadcastReceiver;
520        public IntentFilter intentFilter;
521        public Context context;
522        public Throwable exception;
523
524        public Wrapper(BroadcastReceiver broadcastReceiver, IntentFilter intentFilter, Context context) {
525            this.broadcastReceiver = broadcastReceiver;
526            this.intentFilter = intentFilter;
527            this.context = context;
528            exception = new Throwable();
529        }
530
531        public BroadcastReceiver getBroadcastReceiver() {
532            return broadcastReceiver;
533        }
534
535        public IntentFilter getIntentFilter() {
536            return intentFilter;
537        }
538
539        public Context getContext() {
540            return context;
541        }
542    }
543}
544