diff options
author | Joey Grover <joeygrover@gmail.com> | 2018-10-12 10:26:09 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-12 10:26:09 -0400 |
commit | f1c3b9460584253c1326e7c0b3a0e6f5575e809e (patch) | |
tree | 5d48474f63331e895ef1a0fef7472d77ef384574 | |
parent | f099f7c6431dd661bd314396722154c57d1499c0 (diff) | |
parent | d23012b933c514edd36b7db068b7934ecdabed77 (diff) | |
download | sdl_android-f1c3b9460584253c1326e7c0b3a0e6f5575e809e.tar.gz |
Merge pull request #900 from XevoInc/fix/tcp_transport_wifi_binding_3
Fix: make sure `MultiplexTcpTransport` creates a TCP socket over Wi-Fi network
4 files changed, 457 insertions, 3 deletions
diff --git a/sdl_android/src/androidTest/java/com/smartdevicelink/test/transport/WiFiSocketFactoryTest.java b/sdl_android/src/androidTest/java/com/smartdevicelink/test/transport/WiFiSocketFactoryTest.java new file mode 100644 index 000000000..ab2b7b52d --- /dev/null +++ b/sdl_android/src/androidTest/java/com/smartdevicelink/test/transport/WiFiSocketFactoryTest.java @@ -0,0 +1,354 @@ +package com.smartdevicelink.test.transport; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Build; +import android.util.Log; + +import com.smartdevicelink.transport.utl.WiFiSocketFactory; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +import javax.net.SocketFactory; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * This is a unit test class for the WiFiSocketFactory class: + * {@link com.smartdevicelink.transport.utl.WiFiSocketFactory} + * + * Requires LOLLIPOP or later since the tests use android.net.NetworkCapabilities class + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class WiFiSocketFactoryTest extends TestCase { + + private static final String TAG = WiFiSocketFactoryTest.class.getSimpleName(); + + private Context mMockContext; + private PackageManager mMockPackageManager; + private ConnectivityManager mMockConnMan; + private SocketFactory mMockSocketFactory; // this is the SocketFactory that creates mWiFiBoundSocket + private Socket mWiFiBoundSocket; + + private enum FactoryRet { + RETURNS_NULL, + RETURNS_CORRECT_FACTORY, + RETURNS_ANOTHER_FACTORY, + } + + private class MockNetworkConfig { + // true to make a null Network + boolean isNull; + // specify the transport type of the Network + int transportType; + // spcify the type of SocketFactory returned from this Network + FactoryRet factoryReturnValue; + + MockNetworkConfig(boolean isNull, int transportType, FactoryRet factoryReturnValue) { + this.isNull = isNull; + this.transportType = transportType; + this.factoryReturnValue = factoryReturnValue; + } + } + + private void setupMockNetworks(MockNetworkConfig[] configs) { + if (configs == null) { + when(mMockConnMan.getAllNetworks()).thenReturn(null); + return; + } + + List<Network> networkList = new ArrayList<Network>(configs.length); + + for (MockNetworkConfig config : configs) { + if (config.isNull) { + networkList.add(null); + continue; + } + + Network network = mock(Network.class); + + NetworkCapabilities networkCapabilities = createNetworkCapabilitiesWithTransport(config.transportType); + when(mMockConnMan.getNetworkCapabilities(network)).thenReturn(networkCapabilities); + + SocketFactory factory = null; + switch (config.factoryReturnValue) { + case RETURNS_NULL: + break; + case RETURNS_CORRECT_FACTORY: + factory = mMockSocketFactory; + break; + case RETURNS_ANOTHER_FACTORY: + // create another mock SocketFactory instance + factory = mock(SocketFactory.class); + break; + } + when(network.getSocketFactory()).thenReturn(factory); + + networkList.add(network); + } + + when(mMockConnMan.getAllNetworks()).thenReturn(networkList.toArray(new Network[networkList.size()])); + } + + private static NetworkCapabilities createNetworkCapabilitiesWithTransport(int transport) { + // Creates a dummy NetworkCapabilities instance. + // Since NetworkCapabilities class is 'final', we cannot create its mock. To create a dummy + // instance, here we use reflection to call its constructor and a method that are marked + // with "@hide". + // It is possible that these methods will not be available in a future version of Android. + // In that case we need to update our code accordingly. + Class<NetworkCapabilities> c = NetworkCapabilities.class; + try { + Method addTransportTypeMethod = c.getMethod("addTransportType", int.class); + addTransportTypeMethod.setAccessible(true); + + NetworkCapabilities instance = c.getDeclaredConstructor().newInstance(); + addTransportTypeMethod.invoke(instance, transport); + Log.e(TAG, "Yes successful"); + return instance; + } catch (Exception e) { + Log.e(TAG, "Failed to create NetworkCapabilities instance using reflection: ", e); + return null; + } + } + + // from https://stackoverflow.com/questions/40300469/mock-build-version-with-mockito + // and https://stackoverflow.com/questions/13755117/android-changing-private-static-final-field-using-java-reflection + private static void setFinalStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); +// Field modifiersField = Field.class.getDeclaredField("modifiers"); + // This call might fail on some devices (for example, Nexus 6 with Android 5.0.1). + // If that's the issue, we might want to introduce PowerMock. + Field modifiersField = Field.class.getDeclaredField("accessFlags"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + mMockContext = mock(Context.class); + mMockPackageManager = mock(PackageManager.class); + mMockConnMan = mock(ConnectivityManager.class); + + when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockContext.getPackageName()).thenReturn("dummyPackageName"); + when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mMockConnMan); + + when(mMockPackageManager.checkPermission(eq(Manifest.permission.ACCESS_NETWORK_STATE), anyString())).thenReturn(PackageManager.PERMISSION_GRANTED); + + mMockSocketFactory = mock(SocketFactory.class); + mWiFiBoundSocket = new Socket(); + when(mMockSocketFactory.createSocket()).thenReturn(mWiFiBoundSocket); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + // test the happy path + public void testWithWiFiNetwork() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret); + } + + // test the case where SDK_INT is less than 21 + /* This is disabled since Travis CI uses an AVD with 5.1.1 and setFinalStatic() doesn't work on it. + public void testPriorToLollipop() throws Exception { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + // simulate SDK_INT to less than LOLLIPOP + int previousSDKInt = Build.VERSION.SDK_INT; + setFinalStatic(Build.VERSION.class.getField("SDK_INT"), Build.VERSION_CODES.KITKAT_WATCH); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + // make sure we revert our change + setFinalStatic(Build.VERSION.class.getField("SDK_INT"), previousSDKInt); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since it is not available prior to LOLLIPOP", + mWiFiBoundSocket, ret); + } + */ + + // test the case where we do not have ACCESS_NETWORK_STATE permission + public void testWithoutPermission() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + // simulate the case where required permission isn't available + when(mMockPackageManager.checkPermission(eq(Manifest.permission.ACCESS_NETWORK_STATE), anyString())).thenReturn(PackageManager.PERMISSION_DENIED); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since we don't have required permission", + mWiFiBoundSocket, ret); + } + + // test the case where context.getPackageManager() returns null + public void testPackageManagerNull() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + // simulate the case where ConnectivityManager isn't available + when(mMockContext.getPackageManager()).thenReturn(null); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since PackageManager isn't available", + mWiFiBoundSocket, ret); + } + + // test the case where getSystemService() returns null + public void testConnectivityManagerNull() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + // simulate the case where ConnectivityManager isn't available + when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(null); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since ConnectivityManager isn't working", + mWiFiBoundSocket, ret); + } + + // test the case where ConnectivityManager returns null for the network list + public void testNetworkListNull() { + setupMockNetworks(null); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since Network list isn't available", + mWiFiBoundSocket, ret); + } + + // test the case where the network list contains a null for Network instance + public void testNetworkListHasNull() { + setupMockNetworks(new MockNetworkConfig[] { + // multiple Network instances in the list, the first one being NULL + new MockNetworkConfig(true, 0, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret); + } + + // test the case where the phone isn't connected to Wi-Fi network + public void testNoWiFiNetwork() { + setupMockNetworks(new MockNetworkConfig[] { + // none of the instances has TRANSPORT_WIFI in their capabilities + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_BLUETOOTH, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_VPN, FactoryRet.RETURNS_ANOTHER_FACTORY), + }); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since Wi-Fi network isn't available", + mWiFiBoundSocket, ret); + } + + // test the case where we get null for SocketFactory + public void testSocketFactoryNull() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_NULL), + }); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since SocketFactory isn't available", + mWiFiBoundSocket, ret); + } + + // test the case where we get a null for SocketFactory, then a valid one for another + public void testSocketFactoryNull2() { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_CELLULAR, FactoryRet.RETURNS_ANOTHER_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_NULL), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret); + } + + // test the case where we get an exception with SocketFactory.createSocket() + public void testFactoryReturnsException() throws IOException { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + when(mMockSocketFactory.createSocket()).thenThrow(new IOException("Dummy IOException for testing!")); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertNotSame("Returned Socket shouldn't be created through SocketFactory since it throws an IOException", + mWiFiBoundSocket, ret); + } + + // Test the case we get multiple Network instances with Wi-Fi transport, and the SocketFactory of + // the first one throws Exception and the other one succeeds. + // This is to simulate Samsung Galaxy S9. + public void testFactoryReturnsException2() throws IOException { + setupMockNetworks(new MockNetworkConfig[] { + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + new MockNetworkConfig(false, NetworkCapabilities.TRANSPORT_WIFI, FactoryRet.RETURNS_CORRECT_FACTORY), + }); + + when(mMockSocketFactory.createSocket()).thenThrow(new IOException("Dummy IOException for testing!")) + .thenReturn(mWiFiBoundSocket); + + Socket ret = WiFiSocketFactory.createSocket(mMockContext); + + assertNotNull("createSocket() should always return a Socket instance", ret); + assertEquals("Returned Socket should be created through SocketFactory", mWiFiBoundSocket, ret); + } +} diff --git a/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexTcpTransport.java b/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexTcpTransport.java index 71dd8f6ee..d5843f4c2 100644 --- a/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexTcpTransport.java +++ b/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexTcpTransport.java @@ -32,6 +32,7 @@ package com.smartdevicelink.transport; +import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -39,6 +40,7 @@ import android.util.Log; import com.smartdevicelink.protocol.SdlPacket; import com.smartdevicelink.transport.enums.TransportType; +import com.smartdevicelink.transport.utl.WiFiSocketFactory; import java.io.IOException; import java.io.InputStream; @@ -65,14 +67,16 @@ public class MultiplexTcpTransport extends MultiplexBaseTransport { private OutputStream mOutputStream = null; private MultiplexTcpTransport.TcpTransportThread mThread = null; private WriterThread writerThread; + private Context mContext; - public MultiplexTcpTransport(int port, String ipAddress, boolean autoReconnect, Handler handler) { + public MultiplexTcpTransport(int port, String ipAddress, boolean autoReconnect, Handler handler, Context context) { super(handler, TransportType.TCP); this.ipAddress = ipAddress; this.port = port; connectedDeviceAddress = ipAddress + ":" + port; this.autoReconnect = autoReconnect; + mContext = context; setState(STATE_NONE); } @@ -214,7 +218,7 @@ public class MultiplexTcpTransport extends MultiplexBaseTransport { } logInfo(String.format("TCPTransport.connect: Socket is closed. Trying to connect to %s", getAddress())); - mSocket = new Socket(); + mSocket = WiFiSocketFactory.createSocket(mContext); mSocket.connect(new InetSocketAddress(ipAddress, port)); mOutputStream = mSocket.getOutputStream(); mInputStream = mSocket.getInputStream(); diff --git a/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java b/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java index ec61a13e3..50c5b5412 100644 --- a/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java +++ b/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java @@ -650,7 +650,7 @@ public class SdlRouterService extends Service{ }//else { TCP transport does not exists.} //TCP transport either doesn't exist or is not connected. Start one up. - service.tcpTransport = new MultiplexTcpTransport(port, ipAddress, true, service.tcpHandler); + service.tcpTransport = new MultiplexTcpTransport(port, ipAddress, true, service.tcpHandler, service); service.tcpTransport.start(); } diff --git a/sdl_android/src/main/java/com/smartdevicelink/transport/utl/WiFiSocketFactory.java b/sdl_android/src/main/java/com/smartdevicelink/transport/utl/WiFiSocketFactory.java new file mode 100644 index 000000000..b35070d89 --- /dev/null +++ b/sdl_android/src/main/java/com/smartdevicelink/transport/utl/WiFiSocketFactory.java @@ -0,0 +1,96 @@ +package com.smartdevicelink.transport.utl; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Build; + +import java.io.IOException; +import java.net.Socket; + +import javax.net.SocketFactory; + +import static com.smartdevicelink.util.NativeLogTool.logInfo; + +public class WiFiSocketFactory { + /** + * Try to create a TCP socket which is bound to Wi-Fi network (for Android 5+) + * + * On Android 5 and later, this method tries to create a Socket instance which is bound to a + * Wi-Fi network. If the phone is not connected to a Wi-Fi network, or the app lacks + * required permission (ACCESS_NETWORK_STATE), then this method simply creates a Socket instance + * with "new Socket();". + * + * @return a Socket instance, preferably bound to a Wi-Fi network + */ + public static Socket createSocket(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Socket socket = createWiFiSocket(context); + if (socket != null) { + logInfo("Created a Socket bound to Wi-Fi network"); + return socket; + } + } + + return new Socket(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static Socket createWiFiSocket(Context context) { + PackageManager pm = context.getPackageManager(); + if (pm == null) { + logInfo("PackageManager isn't available."); + return null; + } + // getAllNetworks() and getNetworkCapabilities() require ACCESS_NETWORK_STATE + if (pm.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, context.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + logInfo("Router service doesn't have ACCESS_NETWORK_STATE permission. It cannot bind a TCP transport to Wi-Fi network."); + return null; + } + + ConnectivityManager connMan = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connMan == null) { + logInfo("ConnectivityManager isn't available."); + return null; + } + + Network[] allNetworks = connMan.getAllNetworks(); + if (allNetworks == null) { + logInfo("Failed to acquire a list of networks."); + return null; + } + + // Samsung Galaxy S9 (with Android 8.0.0) provides two `Network` instances which have + // TRANSPORT_WIFI capability. The first one throws an IOException upon creating a Socket, + // and the second one actually works. To support such case, here we iterate over all + // `Network` instances until we can create a Socket. + for (Network network : allNetworks) { + if (network == null) { + continue; + } + + NetworkCapabilities capabilities = connMan.getNetworkCapabilities(network); + if (capabilities == null) { + continue; + } + + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + try { + SocketFactory factory = network.getSocketFactory(); + if (factory != null) { + return factory.createSocket(); + } + } catch (IOException e) { + logInfo("IOException during socket creation (ignored): " + e.getMessage()); + } + } + } + + logInfo("Cannot find Wi-Fi network to bind a TCP transport."); + return null; + } +} |