Move a bunch of @Implementation methods from ShadowApplication to ShadowContextWrapper.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java
index 1b3dbe8..aed4649 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplication.java
@@ -1,48 +1,26 @@
package org.robolectric.shadows;
-import static android.content.pm.PackageManager.PERMISSION_DENIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static com.google.common.util.concurrent.Futures.immediateFuture;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.robolectric.shadow.api.Shadow.newInstanceOf;
import android.app.ActivityThread;
import android.app.Application;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
import android.os.PowerManager;
import android.view.LayoutInflater;
import android.widget.ListPopupWindow;
import android.widget.PopupWindow;
import android.widget.Toast;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
import org.robolectric.RoboSettings;
import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
@@ -53,14 +31,6 @@
public class ShadowApplication extends ShadowContextWrapper {
@RealObject private Application realApplication;
- private List<Intent.FilterComparison> startedServices = new ArrayList<>();
- private List<Intent.FilterComparison> stoppedServices = new ArrayList<>();
- private List<Intent> broadcastIntents = new ArrayList<>();
- private List<ServiceConnection> boundServiceConnections = new ArrayList<>();
- private List<ServiceConnection> unboundServiceConnections = new ArrayList<>();
- private List<Wrapper> registeredReceivers = new ArrayList<>();
- private Map<String, Intent> stickyIntents = new LinkedHashMap<>();
- private Handler mainHandler;
private Scheduler backgroundScheduler = RoboSettings.isUseGlobalScheduler() ? getForegroundThreadScheduler() : new Scheduler();
private List<android.widget.Toast> shownToasts = new ArrayList<>();
private PowerManager.WakeLock latestWakeLock;
@@ -68,18 +38,10 @@
private ShadowDialog latestDialog;
private ShadowPopupMenu latestPopupMenu;
private Object bluetoothAdapter = newInstanceOf("android.bluetooth.BluetoothAdapter");
- private Set<String> grantedPermissions = new HashSet<>();
-
- private boolean unbindServiceShouldThrowIllegalArgument = false;
- private Map<Intent.FilterComparison, ServiceConnectionDataWrapper> serviceConnectionDataForIntent = new HashMap<>();
- private Map<ServiceConnection, ServiceConnectionDataWrapper> serviceConnectionDataForServiceConnection = new HashMap<>();
- //default values for bindService
- private ServiceConnectionDataWrapper defaultServiceConnectionData = new ServiceConnectionDataWrapper(null, null);
// these are managed by the AppSingletonizer... kinda gross, sorry [xw]
LayoutInflater layoutInflater;
AppWidgetManager appWidgetManager;
- private List<String> unbindableActions = new ArrayList<>();
private PopupWindow latestPopupWindow;
@@ -101,7 +63,6 @@
/**
* Attaches an application to a base context.
*
- * @param application The application to attach.
* @param context The context with which to initialize the application, whose base context will
* be attached to the application
*/
@@ -132,416 +93,6 @@
return backgroundScheduler;
}
- @Implementation
- public ComponentName startService(Intent intent) {
- startedServices.add(new Intent.FilterComparison(intent));
- if (intent.getComponent() != null) {
- return intent.getComponent();
- }
- return new ComponentName("some.service.package", "SomeServiceName-FIXME");
- }
-
- @Implementation
- public boolean stopService(Intent name) {
- stoppedServices.add(new Intent.FilterComparison(name));
- return startedServices.contains(new Intent.FilterComparison(name));
- }
-
- public void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) {
- defaultServiceConnectionData = new ServiceConnectionDataWrapper(name, service);
- }
-
- public void setComponentNameAndServiceForBindServiceForIntent(Intent intent, ComponentName name, IBinder service) {
- serviceConnectionDataForIntent.put(new Intent.FilterComparison(intent),
- new ServiceConnectionDataWrapper(name, service));
- }
-
- @Implementation
- public boolean bindService(final Intent intent, final ServiceConnection serviceConnection, int i) {
- boundServiceConnections.add(serviceConnection);
- unboundServiceConnections.remove(serviceConnection);
- if (unbindableActions.contains(intent.getAction())) {
- return false;
- }
- startedServices.add(new Intent.FilterComparison(intent));
- ShadowLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
- shadowLooper.post(() -> {
- final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
- final Intent.FilterComparison filterComparison = new Intent.FilterComparison(intent);
- if (serviceConnectionDataForIntent.containsKey(filterComparison)) {
- serviceConnectionDataWrapper = serviceConnectionDataForIntent.get(filterComparison);
- } else {
- serviceConnectionDataWrapper = defaultServiceConnectionData;
- }
- serviceConnectionDataForServiceConnection.put(serviceConnection, serviceConnectionDataWrapper);
- serviceConnection.onServiceConnected(serviceConnectionDataWrapper.componentNameForBindService, serviceConnectionDataWrapper.binderForBindService);
- }, 0);
- return true;
- }
-
- public List<ServiceConnection> getBoundServiceConnections() {
- return boundServiceConnections;
- }
-
- public void setUnbindServiceShouldThrowIllegalArgument(boolean flag) {
- unbindServiceShouldThrowIllegalArgument = flag;
- }
-
- @Implementation
- public void unbindService(final ServiceConnection serviceConnection) {
- if (unbindServiceShouldThrowIllegalArgument) {
- throw new IllegalArgumentException();
- }
-
- unboundServiceConnections.add(serviceConnection);
- boundServiceConnections.remove(serviceConnection);
- ShadowLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
- shadowLooper.post(() -> {
- final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
- if (serviceConnectionDataForServiceConnection.containsKey(serviceConnection)) {
- serviceConnectionDataWrapper = serviceConnectionDataForServiceConnection.get(serviceConnection);
- } else {
- serviceConnectionDataWrapper = defaultServiceConnectionData;
- }
- serviceConnection.onServiceDisconnected(serviceConnectionDataWrapper.componentNameForBindService);
- }, 0);
- }
-
- public List<ServiceConnection> getUnboundServiceConnections() {
- return unboundServiceConnections;
- }
-
- /**
- * 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).getIntent();
- }
- }
-
- /**
- * 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).getIntent();
- }
- }
-
- /**
- * 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 (stoppedServices.isEmpty()) {
- return null;
- } else {
- return stoppedServices.remove(0).getIntent();
- }
- }
-
- @Implementation
- public void sendBroadcast(Intent intent) {
- sendBroadcastWithPermission(intent, null);
- }
-
- @Implementation
- public void sendBroadcast(Intent intent, String receiverPermission) {
- sendBroadcastWithPermission(intent, receiverPermission);
- }
-
- @Implementation
- public void sendOrderedBroadcast(Intent intent, String receiverPermission) {
- sendOrderedBroadcastWithPermission(intent, receiverPermission);
- }
-
- @Implementation
- public void sendOrderedBroadcast(Intent intent, String receiverPermission, BroadcastReceiver resultReceiver,
- Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
- List<Wrapper> receivers = getAppropriateWrappers(intent, receiverPermission);
- sortByPriority(receivers);
- receivers.add(new Wrapper(resultReceiver, null, this.realApplication, null, scheduler));
- postOrderedToWrappers(receivers, intent, initialCode, initialData, initialExtras);
- }
-
- /*
- Returns the BroadcaseReceivers wrappers, matching intent's action and permissions.
- */
- private List<Wrapper> getAppropriateWrappers(Intent intent, String receiverPermission) {
- broadcastIntents.add(intent);
-
- List<Wrapper> result = new ArrayList<>();
-
- List<Wrapper> copy = new ArrayList<>();
- copy.addAll(registeredReceivers);
- for (Wrapper wrapper : copy) {
- if (hasMatchingPermission(wrapper.broadcastPermission, receiverPermission)
- && wrapper.intentFilter.matchAction(intent.getAction())) {
- final int match = wrapper.intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
- if (match != IntentFilter.NO_MATCH_DATA && match != IntentFilter.NO_MATCH_TYPE) {
- result.add(wrapper);
- }
- }
- }
- return result;
- }
-
- private void postIntent(Intent intent, Wrapper wrapper, final AtomicBoolean abort) {
- final Handler scheduler = (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler();
- final BroadcastReceiver receiver = wrapper.broadcastReceiver;
- final ShadowBroadcastReceiver shReceiver = Shadow.extract(receiver);
- final Intent broadcastIntent = intent;
- scheduler.post(new Runnable() {
- @Override
- public void run() {
- receiver.setPendingResult(ShadowBroadcastPendingResult.create(0, null, null, false));
- shReceiver.onReceive(realApplication, broadcastIntent, abort);
- }
- });
- }
-
- private void postToWrappers(List<Wrapper> wrappers, Intent intent) {
- AtomicBoolean abort = new AtomicBoolean(false); // abort state is shared among all broadcast receivers
- for (Wrapper wrapper: wrappers) {
- postIntent(intent, wrapper, abort);
- }
- }
-
- private void postOrderedToWrappers(List<Wrapper> wrappers, final Intent intent, int initialCode, String data, Bundle extras) {
- final AtomicBoolean abort = new AtomicBoolean(false); // abort state is shared among all broadcast receivers
- ListenableFuture<BroadcastResultHolder> future = immediateFuture(new BroadcastResultHolder(initialCode, data, extras));
- for (final Wrapper wrapper : wrappers) {
- future = postIntent(wrapper, intent, future, abort);
- }
- final ListenableFuture<?> finalFuture = future;
- future.addListener(new Runnable() {
- @Override
- public void run() {
- getMainHandler().post(new Runnable() {
- @Override
- public void run() {
- try {
- finalFuture.get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
- }, directExecutor());
- }
-
- /** Enforces that BroadcastReceivers invoked during an ordered broadcast run serially, passing along their results.*/
- private ListenableFuture<BroadcastResultHolder> postIntent(final Wrapper wrapper,
- final Intent intent,
- ListenableFuture<BroadcastResultHolder> oldResult,
- final AtomicBoolean abort) {
- final Handler scheduler = (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler();
- return Futures.transformAsync(oldResult, new AsyncFunction<BroadcastResultHolder, BroadcastResultHolder>() {
- @Override
- public ListenableFuture<BroadcastResultHolder> apply(BroadcastResultHolder broadcastResultHolder) throws Exception {
- final BroadcastReceiver.PendingResult result = ShadowBroadcastPendingResult.create(
- broadcastResultHolder.resultCode,
- broadcastResultHolder.resultData,
- broadcastResultHolder.resultExtras,
- true /*ordered */);
- wrapper.broadcastReceiver.setPendingResult(result);
- scheduler.post(() -> {
- ShadowBroadcastReceiver shadowBroadcastReceiver =
- Shadow.extract(wrapper.broadcastReceiver);
- shadowBroadcastReceiver.onReceive(realApplication, intent, abort);
- });
- return BroadcastResultHolder.transform(result);
- }
-
- }, directExecutor());
- }
-
- private static final class BroadcastResultHolder {
- private final int resultCode;
- private final String resultData;
- private final Bundle resultExtras;
-
- private BroadcastResultHolder(int resultCode, String resultData, Bundle resultExtras) {
- this.resultCode = resultCode;
- this.resultData = resultData;
- this.resultExtras = resultExtras;
- }
-
- private static ListenableFuture<BroadcastResultHolder> transform(BroadcastReceiver.PendingResult result) {
- ShadowBroadcastPendingResult shadowBroadcastPendingResult = Shadow.extract(result);
- return Futures.transform(shadowBroadcastPendingResult.getFuture(),
- pendingResult -> new BroadcastResultHolder(pendingResult.getResultCode(),
- pendingResult.getResultData(),
- pendingResult.getResultExtras(false)), directExecutor());
- }
- }
-
- /**
- * Broadcasts the {@code Intent} by iterating through the registered receivers, invoking their filters including
- * permissions, and calling {@code onReceive(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
- */
- private void sendBroadcastWithPermission(Intent intent, String receiverPermission) {
- List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
- postToWrappers(wrappers, intent);
- }
-
- private void sendOrderedBroadcastWithPermission(Intent intent, String receiverPermission) {
- List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
- // sort by the decrease of priorities
- sortByPriority(wrappers);
-
- postOrderedToWrappers(wrappers, intent, 0, null, null);
- }
-
- private void sortByPriority(List<Wrapper> wrappers) {
- Collections.sort(wrappers, new Comparator<Wrapper>() {
- @Override
- public int compare(Wrapper o1, Wrapper o2) {
- return Integer.compare(o2.getIntentFilter().getPriority(), o1.getIntentFilter().getPriority());
- }
- });
- }
-
- @Override public List<Intent> getBroadcastIntents() {
- return broadcastIntents;
- }
-
- @Implementation
- public void sendStickyBroadcast(Intent intent) {
- stickyIntents.put(intent.getAction(), intent);
- sendBroadcast(intent);
- }
-
- /**
- * Always returns {@code null}
- *
- * @return {@code null}
- */
- @Implementation
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- return registerReceiverWithContext(receiver, filter, null, null, realApplication);
- }
-
- @Implementation
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) {
- return registerReceiverWithContext(receiver, filter, broadcastPermission, scheduler, realApplication);
- }
-
- Intent registerReceiverWithContext(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context) {
- if (receiver != null) {
- registeredReceivers.add(new Wrapper(receiver, filter, context, broadcastPermission, scheduler));
- }
- return processStickyIntents(filter, receiver, context);
- }
-
- private Intent processStickyIntents(IntentFilter filter, BroadcastReceiver receiver, Context context) {
- Intent result = null;
- for (Intent stickyIntent : stickyIntents.values()) {
- if (filter.matchAction(stickyIntent.getAction())) {
- if (result == null) {
- result = stickyIntent;
- }
- if (receiver != null) {
- receiver.setPendingResult(ShadowBroadcastPendingResult.createSticky(stickyIntent));
- receiver.onReceive(context, stickyIntent);
- receiver.setPendingResult(null);
- } else if (result != null) {
- break;
- }
- }
- }
- return result;
- }
-
- @Implementation
- public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
- boolean found = false;
- Iterator<Wrapper> 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);
- }
- }
-
- public void assertNoBroadcastListenersOfActionRegistered(ContextWrapper context, String action) {
- for (Wrapper registeredReceiver : registeredReceivers) {
- if (registeredReceiver.context == context.getBaseContext()) {
- Iterator<String> 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;
- }
- }
- }
- }
- }
-
- /** @deprecated use PackageManager.queryBroadcastReceivers instead */
- @Deprecated
- public boolean hasReceiverForIntent(Intent intent) {
- for (Wrapper wrapper : registeredReceivers) {
- if (wrapper.intentFilter.matchAction(intent.getAction())) {
- return true;
- }
- }
- return false;
- }
-
- /** @deprecated use PackageManager.queryBroadcastReceivers instead */
- @Deprecated
- public List<BroadcastReceiver> getReceiversForIntent(Intent intent) {
- ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
- for (Wrapper wrapper : registeredReceivers) {
- if (wrapper.intentFilter.matchAction(intent.getAction())) {
- broadcastReceivers.add(wrapper.getBroadcastReceiver());
- }
- }
- return broadcastReceivers;
- }
-
- /**
- * @return list of {@link Wrapper}s for registered receivers
- */
- public List<Wrapper> getRegisteredReceivers() {
- return registeredReceivers;
- }
-
/**
* @return the layout inflater used by this {@code Application}
*/
@@ -576,10 +127,6 @@
return bluetoothAdapter;
}
- public void declareActionUnbindable(String action) {
- unbindableActions.add(action);
- }
-
public PowerManager.WakeLock getLatestWakeLock() {
return latestWakeLock;
}
@@ -675,42 +222,6 @@
}
}
- @Implementation
- public int checkPermission(String permission, int pid, int uid) {
- return grantedPermissions.contains(permission) ? PERMISSION_GRANTED : PERMISSION_DENIED;
- }
-
- @Override public void grantPermissions(String... permissionNames) {
- Collections.addAll(grantedPermissions, permissionNames);
- }
-
- @Override public void denyPermissions(String... permissionNames) {
- for (String permissionName : permissionNames) {
- grantedPermissions.remove(permissionName);
- }
- }
-
- private boolean hasMatchingPermission(String permission1, String permission2) {
- return permission1 == null ? permission2 == null : permission1.equals(permission2);
- }
-
- private static class ServiceConnectionDataWrapper {
- public final ComponentName componentNameForBindService;
- public final IBinder binderForBindService;
-
- private ServiceConnectionDataWrapper(ComponentName componentNameForBindService, IBinder binderForBindService) {
- this.componentNameForBindService = componentNameForBindService;
- this.binderForBindService = binderForBindService;
- }
- }
-
- private Handler getMainHandler() {
- if (mainHandler == null) {
- mainHandler = new Handler(realApplication.getMainLooper());
- }
- return mainHandler;
- }
-
/**
* @deprecated Do not depend on this method to override services as it will be removed in a future update.
* The preferered method is use the shadow of the corresponding service.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
index 28e8883..e0e7621 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
@@ -1,18 +1,19 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.*;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.XmlResourceParser;
-import android.os.Build;
-import android.os.Build.VERSION_CODES;
import android.os.ParcelFileDescriptor;
import android.util.SparseArray;
import android.util.TypedValue;
@@ -422,7 +423,7 @@
@HiddenApi @Implementation(maxSdk = O_MR1)
abstract protected Number getNativeStringBlock(int block);
- @Implementation(maxSdk = O_MR1)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
abstract public SparseArray<String> getAssignedPackageIdentifiers();
@HiddenApi @Implementation(maxSdk = O_MR1)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextWrapper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextWrapper.java
index 99bcb17..d585080 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextWrapper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextWrapper.java
@@ -1,18 +1,232 @@
package org.robolectric.shadows;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
import android.app.ActivityThread;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowApplication.Wrapper;
@Implements(ContextWrapper.class)
public class ShadowContextWrapper {
+ @RealObject
+ private ContextWrapper realContextWrapper;
+ private List<Intent.FilterComparison> startedServices = new ArrayList<>();
+ private List<Intent.FilterComparison> stoppedServices = new ArrayList<>();
+ private List<Intent> broadcastIntents = new ArrayList<>();
+ private List<ServiceConnection> boundServiceConnections = new ArrayList<>();
+ private List<ServiceConnection> unboundServiceConnections = new ArrayList<>();
+ private List<Wrapper> registeredReceivers = new ArrayList<>();
+ private Set<String> grantedPermissions = new HashSet<>();
+ private boolean unbindServiceShouldThrowIllegalArgument = false;
+ private Map<Intent.FilterComparison, ServiceConnectionDataWrapper> serviceConnectionDataForIntent = new HashMap<>();
+ //default values for bindService
+ private ServiceConnectionDataWrapper defaultServiceConnectionData = new ServiceConnectionDataWrapper(null, null);
+ private List<String> unbindableActions = new ArrayList<>();
+ private Map<String, Intent> stickyIntents = new LinkedHashMap<>();
+ private Handler mainHandler;
+ private Map<ServiceConnection, ServiceConnectionDataWrapper> serviceConnectionDataForServiceConnection = new HashMap<>();
+
+ @Implementation
+ public void sendBroadcast(Intent intent) {
+ sendBroadcastWithPermission(intent, null);
+ }
+
+ @Implementation
+ public void sendBroadcast(Intent intent, String receiverPermission) {
+ sendBroadcastWithPermission(intent, receiverPermission);
+ }
+
+ @Implementation
+ public void sendOrderedBroadcast(Intent intent, String receiverPermission) {
+ sendOrderedBroadcastWithPermission(intent, receiverPermission);
+ }
+
+ @Implementation
+ public void sendOrderedBroadcast(Intent intent, String receiverPermission, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
+ List<Wrapper> receivers = getAppropriateWrappers(intent, receiverPermission);
+ sortByPriority(receivers);
+ receivers.add(new Wrapper(resultReceiver, null, this.realContextWrapper, null, scheduler));
+ postOrderedToWrappers(receivers, intent, initialCode, initialData, initialExtras);
+ }
+
+ public void assertNoBroadcastListenersOfActionRegistered(ContextWrapper context, String action) {
+ for (Wrapper registeredReceiver : registeredReceivers) {
+ if (registeredReceiver.context == context.getBaseContext()) {
+ Iterator<String> 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;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the BroadcaseReceivers wrappers, matching intent's action and permissions.
+ */
+ private List<Wrapper> getAppropriateWrappers(Intent intent, String receiverPermission) {
+ broadcastIntents.add(intent);
+
+ List<Wrapper> result = new ArrayList<>();
+
+ List<Wrapper> copy = new ArrayList<>();
+ copy.addAll(registeredReceivers);
+ for (Wrapper wrapper : copy) {
+ if (hasMatchingPermission(wrapper.broadcastPermission, receiverPermission)
+ && wrapper.intentFilter.matchAction(intent.getAction())) {
+ final int match = wrapper.intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
+ if (match != IntentFilter.NO_MATCH_DATA && match != IntentFilter.NO_MATCH_TYPE) {
+ result.add(wrapper);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void postIntent(Intent intent, Wrapper wrapper, final AtomicBoolean abort) {
+ final Handler scheduler = (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler();
+ final BroadcastReceiver receiver = wrapper.broadcastReceiver;
+ final ShadowBroadcastReceiver shReceiver = Shadow.extract(receiver);
+ final Intent broadcastIntent = intent;
+ scheduler.post(new Runnable() {
+ @Override
+ public void run() {
+ receiver.setPendingResult(ShadowBroadcastPendingResult.create(0, null, null, false));
+ shReceiver.onReceive(realContextWrapper, broadcastIntent, abort);
+ }
+ });
+ }
+
+ private void postToWrappers(List<Wrapper> wrappers, Intent intent) {
+ AtomicBoolean abort = new AtomicBoolean(false); // abort state is shared among all broadcast receivers
+ for (Wrapper wrapper: wrappers) {
+ postIntent(intent, wrapper, abort);
+ }
+ }
+
+ private void postOrderedToWrappers(List<Wrapper> wrappers, final Intent intent, int initialCode, String data, Bundle extras) {
+ final AtomicBoolean abort = new AtomicBoolean(false); // abort state is shared among all broadcast receivers
+ ListenableFuture<BroadcastResultHolder> future = immediateFuture(new BroadcastResultHolder(initialCode, data, extras));
+ for (final Wrapper wrapper : wrappers) {
+ future = postIntent(wrapper, intent, future, abort);
+ }
+ final ListenableFuture<?> finalFuture = future;
+ future.addListener(new Runnable() {
+ @Override
+ public void run() {
+ getMainHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ finalFuture.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+ }, directExecutor());
+ }
+
+ /** Enforces that BroadcastReceivers invoked during an ordered broadcast run serially, passing along their results.*/
+ private ListenableFuture<BroadcastResultHolder> postIntent(final Wrapper wrapper,
+ final Intent intent,
+ ListenableFuture<BroadcastResultHolder> oldResult,
+ final AtomicBoolean abort) {
+ final Handler scheduler = (wrapper.scheduler != null) ? wrapper.scheduler : getMainHandler();
+ return Futures
+ .transformAsync(oldResult, new AsyncFunction<BroadcastResultHolder, BroadcastResultHolder>() {
+ @Override
+ public ListenableFuture<BroadcastResultHolder> apply(BroadcastResultHolder broadcastResultHolder) throws Exception {
+ final BroadcastReceiver.PendingResult result = ShadowBroadcastPendingResult.create(
+ broadcastResultHolder.resultCode,
+ broadcastResultHolder.resultData,
+ broadcastResultHolder.resultExtras,
+ true /*ordered */);
+ wrapper.broadcastReceiver.setPendingResult(result);
+ scheduler.post(() -> {
+ ShadowBroadcastReceiver shadowBroadcastReceiver =
+ Shadow.extract(wrapper.broadcastReceiver);
+ shadowBroadcastReceiver.onReceive(realContextWrapper, intent, abort);
+ });
+ return BroadcastResultHolder.transform(result);
+ }
+
+ }, directExecutor());
+ }
+
+ /**
+ * Broadcasts the {@code Intent} by iterating through the registered receivers, invoking their filters including
+ * permissions, and calling {@code onReceive(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
+ */
+ private void sendBroadcastWithPermission(Intent intent, String receiverPermission) {
+ List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
+ postToWrappers(wrappers, intent);
+ }
+
+ private void sendOrderedBroadcastWithPermission(Intent intent, String receiverPermission) {
+ List<Wrapper> wrappers = getAppropriateWrappers(intent, receiverPermission);
+ // sort by the decrease of priorities
+ sortByPriority(wrappers);
+
+ postOrderedToWrappers(wrappers, intent, 0, null, null);
+ }
+
+ private void sortByPriority(List<Wrapper> wrappers) {
+ Collections.sort(wrappers, new Comparator<Wrapper>() {
+ @Override
+ public int compare(Wrapper o1, Wrapper o2) {
+ return Integer.compare(o2.getIntentFilter().getPriority(), o1.getIntentFilter().getPriority());
+ }
+ });
+ }
+
public List<Intent> getBroadcastIntents() {
- return ShadowApplication.getInstance().getBroadcastIntents();
+ return broadcastIntents;
}
/**
@@ -39,47 +253,283 @@
return shadowInstrumentation.peekNextStartedActivity();
}
+ @Implementation
+ public ComponentName startService(Intent intent) {
+ startedServices.add(new Intent.FilterComparison(intent));
+ if (intent.getComponent() != null) {
+ return intent.getComponent();
+ }
+ return new ComponentName("some.service.package", "SomeServiceName-FIXME");
+ }
+
+ @Implementation
+ public boolean stopService(Intent name) {
+ stoppedServices.add(new Intent.FilterComparison(name));
+ return startedServices.contains(new Intent.FilterComparison(name));
+ }
+
+ public void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) {
+ defaultServiceConnectionData = new ServiceConnectionDataWrapper(name, service);
+ }
+
+ public void setComponentNameAndServiceForBindServiceForIntent(Intent intent, ComponentName name, IBinder service) {
+ serviceConnectionDataForIntent.put(new Intent.FilterComparison(intent),
+ new ServiceConnectionDataWrapper(name, service));
+ }
+
+ @Implementation
+ public boolean bindService(final Intent intent, final ServiceConnection serviceConnection, int i) {
+ boundServiceConnections.add(serviceConnection);
+ unboundServiceConnections.remove(serviceConnection);
+ if (unbindableActions.contains(intent.getAction())) {
+ return false;
+ }
+ startedServices.add(new Intent.FilterComparison(intent));
+ ShadowLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
+ shadowLooper.post(() -> {
+ final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
+ final Intent.FilterComparison filterComparison = new Intent.FilterComparison(intent);
+ if (serviceConnectionDataForIntent.containsKey(filterComparison)) {
+ serviceConnectionDataWrapper = serviceConnectionDataForIntent.get(filterComparison);
+ } else {
+ serviceConnectionDataWrapper = defaultServiceConnectionData;
+ }
+ serviceConnectionDataForServiceConnection.put(serviceConnection, serviceConnectionDataWrapper);
+ serviceConnection.onServiceConnected(serviceConnectionDataWrapper.componentNameForBindService, serviceConnectionDataWrapper.binderForBindService);
+ }, 0);
+ return true;
+ }
+
+ @Implementation
+ public void unbindService(final ServiceConnection serviceConnection) {
+ if (unbindServiceShouldThrowIllegalArgument) {
+ throw new IllegalArgumentException();
+ }
+
+ unboundServiceConnections.add(serviceConnection);
+ boundServiceConnections.remove(serviceConnection);
+ ShadowLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
+ shadowLooper.post(() -> {
+ final ServiceConnectionDataWrapper serviceConnectionDataWrapper;
+ if (serviceConnectionDataForServiceConnection.containsKey(serviceConnection)) {
+ serviceConnectionDataWrapper = serviceConnectionDataForServiceConnection.get(serviceConnection);
+ } else {
+ serviceConnectionDataWrapper = defaultServiceConnectionData;
+ }
+ serviceConnection.onServiceDisconnected(serviceConnectionDataWrapper.componentNameForBindService);
+ }, 0);
+ }
+
+ public List<ServiceConnection> getBoundServiceConnections() {
+ return boundServiceConnections;
+ }
+
+ public void setUnbindServiceShouldThrowIllegalArgument(boolean flag) {
+ unbindServiceShouldThrowIllegalArgument = flag;
+ }
+
+ public List<ServiceConnection> getUnboundServiceConnections() {
+ return unboundServiceConnections;
+ }
+
+ public void declareActionUnbindable(String action) {
+ unbindableActions.add(action);
+ }
+
/**
- * Delegates to the application to consume and return the next {@code Intent} on the
- * started services stack.
+ * Consumes the most recent {@code Intent} started by
+ * {@link #startService(android.content.Intent)} and returns it.
*
- * @return the next started {@code Intent} for a service
+ * @return the most recently started {@code Intent}
*/
public Intent getNextStartedService() {
- return ShadowApplication.getInstance().getNextStartedService();
+ if (startedServices.isEmpty()) {
+ return null;
+ } else {
+ return startedServices.remove(0).getIntent();
+ }
}
/**
- * Delegates to the application to clear the stack of started service intents.
- */
- public void clearStartedServices() {
- ShadowApplication.getInstance().clearStartedServices();
- }
-
- /**
- * Return (without consuming) the next {@code Intent} on the started services stack.
+ * Returns the most recent {@code Intent} started by {@link #startService(android.content.Intent)}
+ * without consuming it.
*
- * @return the next started {@code Intent} for a service
+ * @return the most recently started {@code Intent}
*/
public Intent peekNextStartedService() {
- return ShadowApplication.getInstance().peekNextStartedService();
+ if (startedServices.isEmpty()) {
+ return null;
+ } else {
+ return startedServices.get(0).getIntent();
+ }
}
/**
- * Delegates to the application to return the next {@code Intent} to stop
- * a service (irrespective of if the service was running)
- *
- * @return {@code Intent} for the next service requested to be stopped
+ * Clears all {@code Intent} started by {@link #startService(android.content.Intent)}.
+ */
+ 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.
*/
public Intent getNextStoppedService() {
- return ShadowApplication.getInstance().getNextStoppedService();
+ if (stoppedServices.isEmpty()) {
+ return null;
+ } else {
+ return stoppedServices.remove(0).getIntent();
+ }
+ }
+
+ @Implementation
+ public void sendStickyBroadcast(Intent intent) {
+ stickyIntents.put(intent.getAction(), intent);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Always returns {@code null}
+ *
+ * @return {@code null}
+ */
+ @Implementation
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return registerReceiverWithContext(receiver, filter, null, null, realContextWrapper);
+ }
+
+ @Implementation
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ return registerReceiverWithContext(receiver, filter, broadcastPermission, scheduler,
+ realContextWrapper);
+ }
+
+ Intent registerReceiverWithContext(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context) {
+ if (receiver != null) {
+ registeredReceivers.add(new Wrapper(receiver, filter, context, broadcastPermission, scheduler));
+ }
+ return processStickyIntents(filter, receiver, context);
+ }
+
+ private Intent processStickyIntents(IntentFilter filter, BroadcastReceiver receiver, Context context) {
+ Intent result = null;
+ for (Intent stickyIntent : stickyIntents.values()) {
+ if (filter.matchAction(stickyIntent.getAction())) {
+ if (result == null) {
+ result = stickyIntent;
+ }
+ if (receiver != null) {
+ receiver.setPendingResult(ShadowBroadcastPendingResult.createSticky(stickyIntent));
+ receiver.onReceive(context, stickyIntent);
+ receiver.setPendingResult(null);
+ } else if (result != null) {
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ @Implementation
+ public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
+ boolean found = false;
+ Iterator<Wrapper> 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);
+ }
+ }
+
+ /** @deprecated use PackageManager.queryBroadcastReceivers instead */
+ @Deprecated
+ public boolean hasReceiverForIntent(Intent intent) {
+ for (Wrapper wrapper : registeredReceivers) {
+ if (wrapper.intentFilter.matchAction(intent.getAction())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @deprecated use PackageManager.queryBroadcastReceivers instead */
+ @Deprecated
+ public List<BroadcastReceiver> getReceiversForIntent(Intent intent) {
+ ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
+ for (Wrapper wrapper : registeredReceivers) {
+ if (wrapper.intentFilter.matchAction(intent.getAction())) {
+ broadcastReceivers.add(wrapper.getBroadcastReceiver());
+ }
+ }
+ return broadcastReceivers;
+ }
+
+ /**
+ * @return list of {@link Wrapper}s for registered receivers
+ */
+ public List<Wrapper> getRegisteredReceivers() {
+ return registeredReceivers;
+ }
+
+ @Implementation
+ public int checkPermission(String permission, int pid, int uid) {
+ return grantedPermissions.contains(permission) ? PERMISSION_GRANTED : PERMISSION_DENIED;
}
public void grantPermissions(String... permissionNames) {
- ShadowApplication.getInstance().grantPermissions(permissionNames);
+ Collections.addAll(grantedPermissions, permissionNames);
}
public void denyPermissions(String... permissionNames) {
- ShadowApplication.getInstance().denyPermissions(permissionNames);
+ for (String permissionName : permissionNames) {
+ grantedPermissions.remove(permissionName);
+ }
+ }
+
+ private boolean hasMatchingPermission(String permission1, String permission2) {
+ return permission1 == null ? permission2 == null : permission1.equals(permission2);
+ }
+
+ private Handler getMainHandler() {
+ if (mainHandler == null) {
+ mainHandler = new Handler(realContextWrapper.getMainLooper());
+ }
+ return mainHandler;
+ }
+
+ private static final class BroadcastResultHolder {
+ private final int resultCode;
+ private final String resultData;
+ private final Bundle resultExtras;
+
+ private BroadcastResultHolder(int resultCode, String resultData, Bundle resultExtras) {
+ this.resultCode = resultCode;
+ this.resultData = resultData;
+ this.resultExtras = resultExtras;
+ }
+
+ private static ListenableFuture<BroadcastResultHolder> transform(BroadcastReceiver.PendingResult result) {
+ ShadowBroadcastPendingResult shadowBroadcastPendingResult = Shadow.extract(result);
+ return Futures.transform(shadowBroadcastPendingResult.getFuture(),
+ pendingResult -> new BroadcastResultHolder(pendingResult.getResultCode(),
+ pendingResult.getResultData(),
+ pendingResult.getResultExtras(false)), directExecutor());
+ }
+ }
+
+ private static class ServiceConnectionDataWrapper {
+ public final ComponentName componentNameForBindService;
+ public final IBinder binderForBindService;
+
+ private ServiceConnectionDataWrapper(ComponentName componentNameForBindService, IBinder binderForBindService) {
+ this.componentNameForBindService = componentNameForBindService;
+ this.binderForBindService = binderForBindService;
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTabWidget.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTabWidget.java
index 9afb079..556463d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTabWidget.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTabWidget.java
@@ -1,5 +1,7 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.M;
+
import android.widget.TabWidget;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
@@ -8,7 +10,7 @@
@Implements(TabWidget.class)
public class ShadowTabWidget extends ShadowLinearLayout {
- @HiddenApi @Implementation
+ @HiddenApi @Implementation(maxSdk = M)
public void initTabWidget() {
}
}