ConnectivityManager: release all requests mapping to a callback.

This patch changes how callback unregistration works in order to be
consistent with undocumented use cases currently de-facto supported
by the API (although in a buggy way):
  - callback recycling: releasing then reregistering a callback again.
  - multiple request registrations with the same callback.

The second use case is not desirable but needs to be taken into account
for now for the purpose of correctly releasing NetworkRequests
registered in ConnectivityService.

In order to support request release in both use cases with minimal
amount of complexity for the time being the following changes are done:
  - request to callback unmapping is done synchronously at callback
    release time.
  - all requests associated to a callback are unmapped at callback
    release time.

This fixes the following issues:
  - a callback stops being triggered as soon as it is released.
    Otherwise when recycling the callback immediately, it is possible
    the previous request associated with it triggers it, confusing the
    app.
  - when a callback is registered multiple times, the requests are not
    leaked.
  - when a callback is registered multiple times and then released, the
    N-1 first registrations do not trigger the callback anymore.

In the future it would be desirable to enforce the intended 1:1 mapping
between callbacks and requests at registration time.

Bug: 35921499, 35955593, 20701525
Test: - added new tests in ConnectivityManagerTest to test releasing,
      recycling, and a disabled test for no multiple regristration.
      - new tests catch regression causing b/35921499, b/35955593.
Change-Id: Ia0917ac322fc049f76adb4743bc745989fed6d26
This commit is contained in:
Hugo Benichi
2017-03-06 09:17:06 +09:00
parent a0d447ce3e
commit 2aa65af966
2 changed files with 212 additions and 40 deletions

View File

@@ -36,21 +36,50 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.content.pm.ApplicationInfo;
import android.os.Build.VERSION_CODES;
import android.net.ConnectivityManager.NetworkCallback;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.runner.RunWith;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ConnectivityManagerTest {
@Mock Context mCtx;
@Mock IConnectivityManager mService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
static NetworkCapabilities verifyNetworkCapabilities(
int legacyType, int transportType, int... capabilities) {
final NetworkCapabilities nc = ConnectivityManager.networkCapabilitiesForType(legacyType);
@@ -173,4 +202,124 @@ public class ConnectivityManagerTest {
verifyUnrestrictedNetworkCapabilities(
ConnectivityManager.TYPE_ETHERNET, TRANSPORT_ETHERNET);
}
@Test
public void testCallbackRelease() throws Exception {
ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
NetworkRequest request = makeRequest(1);
NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class);
Handler handler = new Handler(Looper.getMainLooper());
ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class);
// register callback
when(mService.requestNetwork(any(), captor.capture(), anyInt(), any(), anyInt()))
.thenReturn(request);
manager.requestNetwork(request, callback, handler);
// callback triggers
captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_AVAILABLE));
verify(callback, timeout(500).times(1)).onAvailable(any());
// unregister callback
manager.unregisterNetworkCallback(callback);
verify(mService, times(1)).releaseNetworkRequest(request);
// callback does not trigger anymore.
captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_LOSING));
verify(callback, timeout(500).times(0)).onLosing(any(), anyInt());
}
@Test
public void testCallbackRecycling() throws Exception {
ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
NetworkRequest req1 = makeRequest(1);
NetworkRequest req2 = makeRequest(2);
NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class);
Handler handler = new Handler(Looper.getMainLooper());
ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class);
// register callback
when(mService.requestNetwork(any(), captor.capture(), anyInt(), any(), anyInt()))
.thenReturn(req1);
manager.requestNetwork(req1, callback, handler);
// callback triggers
captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_AVAILABLE));
verify(callback, timeout(100).times(1)).onAvailable(any());
// unregister callback
manager.unregisterNetworkCallback(callback);
verify(mService, times(1)).releaseNetworkRequest(req1);
// callback does not trigger anymore.
captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_LOSING));
verify(callback, timeout(100).times(0)).onLosing(any(), anyInt());
// callback can be registered again
when(mService.requestNetwork(any(), captor.capture(), anyInt(), any(), anyInt()))
.thenReturn(req2);
manager.requestNetwork(req2, callback, handler);
// callback triggers
captor.getValue().send(makeMessage(req2, ConnectivityManager.CALLBACK_LOST));
verify(callback, timeout(100).times(1)).onLost(any());
// unregister callback
manager.unregisterNetworkCallback(callback);
verify(mService, times(1)).releaseNetworkRequest(req2);
}
// TODO: turn on this test when request callback 1:1 mapping is enforced
//@Test
private void noDoubleCallbackRegistration() throws Exception {
ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
NetworkRequest request = makeRequest(1);
NetworkCallback callback = new ConnectivityManager.NetworkCallback();
ApplicationInfo info = new ApplicationInfo();
// TODO: update version when starting to enforce 1:1 mapping
info.targetSdkVersion = VERSION_CODES.N_MR1 + 1;
when(mCtx.getApplicationInfo()).thenReturn(info);
when(mService.requestNetwork(any(), any(), anyInt(), any(), anyInt())).thenReturn(request);
Handler handler = new Handler(Looper.getMainLooper());
manager.requestNetwork(request, callback, handler);
// callback is already registered, reregistration should fail.
Class<IllegalArgumentException> wantException = IllegalArgumentException.class;
expectThrowable(() -> manager.requestNetwork(request, callback), wantException);
manager.unregisterNetworkCallback(callback);
verify(mService, times(1)).releaseNetworkRequest(request);
// unregistering the callback should make it registrable again.
manager.requestNetwork(request, callback);
}
static Message makeMessage(NetworkRequest req, int messageType) {
Bundle bundle = new Bundle();
bundle.putParcelable(NetworkRequest.class.getSimpleName(), req);
Message msg = Message.obtain();
msg.what = messageType;
msg.setData(bundle);
return msg;
}
static NetworkRequest makeRequest(int requestId) {
NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
return new NetworkRequest(request.networkCapabilities, ConnectivityManager.TYPE_NONE,
requestId, NetworkRequest.Type.NONE);
}
static void expectThrowable(Runnable block, Class<? extends Throwable> throwableType) {
try {
block.run();
} catch (Throwable t) {
if (t.getClass().equals(throwableType)) {
return;
}
fail("expected exception of type " + throwableType + ", but was " + t.getClass());
}
fail("expected exception of type " + throwableType);
}
}