Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Robolectric does not bind service from another thread

I am using Robolectric 3.4.2 and I need to test the interaction between two services.

In my test I wrote a dummy service:

ShadowApplication.getInstance().setComponentNameAndServiceForBindService(
        new ComponentName(SERVICE.getPackageName(), SERVICE.getClassName()),
            new Binder() {
                @Override
                public IInterface queryLocalInterface(final String descriptor) {
                    return null;
                }
            }
    );

and it works if I invoke BindService directly from my test case, but if the call to bindService is in a different thread (like in the real application), the onServiceConnected() callback is never called.

ServiceConnection connection = new ServiceConnection(){

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        try {
            doSomething();
        } catch (Exception e) {
            Log.e(TAG, "Cannot Do Something", e);
        }
        mContext.unbindService(this);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) { }
};

new Thread(new Runnable() {
    @Override
    public void run() {
        mContext.bindService(SERVICE.createIntent()), connection, Context.BIND_AUTO_CREATE);
    }
}).start();

Am I doing something wrong, or is it expected to work this way?

like image 938
G B Avatar asked Sep 16 '25 23:09

G B


1 Answers

I have good news. It works!

To solve the issue let's look at the source code. Let's look bindService method inside ShadowInstrumentation class:

private boolean bindService(
              final Intent intent,
              final ServiceConnection serviceConnection,
              ServiceCallbackScheduler serviceCallbackScheduler) {
            boundServiceConnections.add(serviceConnection);
            unboundServiceConnections.remove(serviceConnection);
            if (exceptionForBindService != null) {
              throw exceptionForBindService;
            }
            final Intent.FilterComparison filterComparison = new Intent.FilterComparison(intent);
            final ServiceConnectionDataWrapper serviceConnectionDataWrapper =
                serviceConnectionDataForIntent.getOrDefault(filterComparison, defaultServiceConnectionData);
            if (unbindableActions.contains(intent.getAction())
                || unbindableComponents.contains(intent.getComponent())
                || unbindableComponents.contains(
                    serviceConnectionDataWrapper.componentNameForBindService)) {
              return false;
            }
            startedServices.add(filterComparison);
            Runnable onServiceConnectedRunnable =
                () -> {
                  serviceConnectionDataForServiceConnection.put(
                      serviceConnection, serviceConnectionDataWrapper);
                  serviceConnection.onServiceConnected(
                      serviceConnectionDataWrapper.componentNameForBindService,
                      serviceConnectionDataWrapper.binderForBindService);
                };
        
            if (bindServiceCallsOnServiceConnectedInline) {
              onServiceConnectedRunnable.run();
            } else {
              serviceCallbackScheduler.schedule(onServiceConnectedRunnable);
            }
            return true;
          }

So to call callback it should run:

 onServiceConnectedRunnable.run();

For this, we should set a flag bindServiceCallsOnServiceConnectedInline in true:

shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(true);

So finally code:

@RunWith(RobolectricTestRunner.class)

public class MyServiceRoboTest {
    private static final String TAG = MyServiceRoboTest.class.getCanonicalName();
    private final Context mContext = ApplicationProvider.getApplicationContext();

        @Before
        public void setUp() {
            ShadowLog.stream = System.out;
            Log.i(TAG, "MyServiceRoboTest setUp");
            MockitoAnnotations.initMocks(this);
    
            MyService service = Robolectric.setupService(MyService.class);
            Assert.assertNotNull(service);
    
            final ShadowApplication shadowApplication =
                    Shadows.shadowOf((Application) mContext);
    
            shadowApplication.setComponentNameAndServiceForBindServiceForIntent(
                    new Intent(mContext, MyService.class),
                    new ComponentName(mContext, MyService.class),
                    service.onBind(null));
    
            shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(true);
        }
    
        @Test
        public void testMyMessageManager() {
            MyManager.ServiceConnectListener serviceConnectListener = Mockito.mock(MyManager.ServiceConnectListener.class);
            MyMessageManager myMessageManager = new MyMessageManager(mContext, serviceConnectListener);

            verify(serviceConnectListener).onServiceConnected();
           
        }
    }
like image 80
Serg Burlaka Avatar answered Sep 18 '25 19:09

Serg Burlaka