diff options
5 files changed, 382 insertions, 18 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java index 54c312d7..8dab49ce 100644 --- a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java @@ -1,3 +1,20 @@ +/** + * Copyright (c) 2020LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + package se.leap.bitmaskclient.tethering; import android.content.BroadcastReceiver; @@ -15,9 +32,13 @@ public class TetheringBroadcastReceiver extends BroadcastReceiver { Log.d(TAG, "TETHERING WIFI_AP_STATE_CHANGED"); int apState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0); if (WifiHotspotState.WIFI_AP_STATE_ENABLED.ordinal() == apState % 10) { - TetheringObservable.setWifiTethering(true); + if (!TetheringObservable.getInstance().isWifiTetheringEnabled()) { + TetheringObservable.setWifiTethering(true); + } } else { - TetheringObservable.setWifiTethering(false); + if (TetheringObservable.getInstance().isWifiTetheringEnabled()) { + TetheringObservable.setWifiTethering(false); + } } } else if ("android.net.conn.TETHER_STATE_CHANGED".equals(intent.getAction())) { Log.d(TAG, "TETHERING TETHER_STATE_CHANGED"); diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java index 1e2521b8..0d4f56d8 100644 --- a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java @@ -1,20 +1,44 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ package se.leap.bitmaskclient.tethering; import android.content.Context; import android.content.IntentFilter; -import android.net.wifi.WifiManager; -import java.lang.reflect.Method; import java.net.NetworkInterface; import java.util.Enumeration; import se.leap.bitmaskclient.utils.Cmd; +/** + * This manager tries to figure out the current tethering states for Wifi, USB and Bluetooth + * The default behavior differs for failing attempts to get these states: + * Wifi: keeps old state + * USB: defaults to false + * Bluetooth defaults to false + * For Wifi there's a second method to check the current state (see TetheringBroadcastReceiver). + * Either of both methods can change the state if they succeed, but are ignored if they fail. + * This should avoid any interference between both methods. + */ public class TetheringStateManager { private static final String TAG = TetheringStateManager.class.getSimpleName(); private static TetheringStateManager instance; - private WifiManager wifiManager; + private WifiManagerWrapper wifiManager; private TetheringStateManager() { } @@ -30,20 +54,14 @@ public class TetheringStateManager { IntentFilter intentFilter = new IntentFilter("android.net.conn.TETHER_STATE_CHANGED"); intentFilter.addAction("android.net.wifi.WIFI_AP_STATE_CHANGED"); context.getApplicationContext().registerReceiver(broadcastReceiver, intentFilter); - instance.wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + instance.wifiManager = new WifiManagerWrapper(context); updateWifiTetheringState(); updateUsbTetheringState(); updateBluetoothTetheringState(); } - private static boolean isWifiApEnabled() { - try { - Method method = instance.wifiManager.getClass().getMethod("getWifiApState"); - int tmp = ((Integer) method.invoke(instance.wifiManager)); - return WifiHotspotState.WIFI_AP_STATE_ENABLED.ordinal() == tmp % 10; - } catch (Exception e) { - return false; - } + private static boolean isWifiApEnabled() throws Exception { + return instance.wifiManager.isWifiAPEnabled(); } @@ -64,7 +82,6 @@ public class TetheringStateManager { return false; } - // Check whether Bluetooth tethering is enabled. private static boolean isBluetoothTetheringEnabled() { StringBuilder log = new StringBuilder(); boolean hasBtPan = false; @@ -87,7 +104,15 @@ public class TetheringStateManager { } static void updateWifiTetheringState() { - TetheringObservable.setWifiTethering(isWifiApEnabled()); + boolean lastState = TetheringObservable.getInstance().isWifiTetheringEnabled(); + try { + boolean currentState = isWifiApEnabled(); + if (currentState != lastState) { + TetheringObservable.setWifiTethering(currentState); + } + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java new file mode 100644 index 00000000..ed395d7f --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import java.lang.reflect.Method; + +/** + * This Wrapper allows better Unit testing. + */ +class WifiManagerWrapper { + + private WifiManager wifiManager; + + WifiManagerWrapper(Context context) { + this.wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + boolean isWifiAPEnabled() throws Exception { + Method method = wifiManager.getClass().getMethod("getWifiApState"); + int tmp = ((Integer) method.invoke(wifiManager)); + return WifiHotspotState.WIFI_AP_STATE_ENABLED.ordinal() == tmp % 10; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java index a72658a4..d033ed24 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java @@ -18,7 +18,6 @@ package se.leap.bitmaskclient.utils; import android.support.annotation.WorkerThread; -import android.util.Log; import java.io.IOException; import java.io.InputStreamReader; @@ -43,7 +42,6 @@ public class Cmd { try { for (String cmd : cmds) { - Log.d(TAG, "executing CMD: " + cmd); out.write(cmd); out.write("\n"); } diff --git a/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java new file mode 100644 index 00000000..295714c3 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.content.Context; +import android.content.IntentFilter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; + +import se.leap.bitmaskclient.utils.Cmd; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + + +@RunWith(PowerMockRunner.class) +@PrepareForTest({WifiManagerWrapper.class, TetheringStateManager.class, Cmd.class, NetworkInterface.class}) +public class TetheringStateManagerTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Context mockContext; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + IntentFilter intentFilter; + + TetheringObservable observable; + + @Before + public void setup() throws Exception { + PowerMockito.whenNew(IntentFilter.class).withArguments(anyString()).thenReturn(intentFilter); + PowerMockito.whenNew(IntentFilter.class).withNoArguments().thenReturn(intentFilter); + observable = TetheringObservable.getInstance(); + + } + + @Test + public void updateUsbTetheringState_findsRndisX_returnsTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + NetworkInterface mock1 = PowerMockito.mock(NetworkInterface.class); + when(mock1.isLoopback()).thenReturn(false); + when(mock1.getName()).thenReturn("eth0"); + NetworkInterface mock2 = PowerMockito.mock(NetworkInterface.class); + when(mock2.isLoopback()).thenReturn(false); + when(mock2.getName()).thenReturn("rndis0"); + + NetworkInterface[] networkInterfaces = new NetworkInterface[2]; + networkInterfaces[0] = mock1; + networkInterfaces[1] = mock2; + + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).then(new Answer<Enumeration<NetworkInterface>>() { + @Override + public Enumeration<NetworkInterface> answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(Arrays.asList(networkInterfaces)); + } + }); + + TetheringObservable.setUsbTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isUsbTetheringEnabled()); + } + + @Test + public void updateUsbTetheringState_doesntFindRndisX_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + NetworkInterface mock1 = PowerMockito.mock(NetworkInterface.class); + when(mock1.isLoopback()).thenReturn(false); + when(mock1.getName()).thenReturn("eth0"); + NetworkInterface mock2 = PowerMockito.mock(NetworkInterface.class); + when(mock2.isLoopback()).thenReturn(false); + when(mock2.getName()).thenReturn("wifi0"); + + NetworkInterface[] networkInterfaces = new NetworkInterface[2]; + networkInterfaces[0] = mock1; + networkInterfaces[1] = mock2; + + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).then(new Answer<Enumeration<NetworkInterface>>() { + @Override + public Enumeration<NetworkInterface> answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(Arrays.asList(networkInterfaces)); + } + }); + + TetheringObservable.setUsbTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isUsbTetheringEnabled()); + } + + @Test + public void updateUsbTetheringState_ThrowsException_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).thenThrow(new SocketException()); + + TetheringObservable.setUsbTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isUsbTetheringEnabled()); + } + + @Test + public void updateBluetoothTetheringState_btDeviceFound_returnTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device found"); + return 0; + } + }); + + TetheringObservable.setBluetoothTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isBluetoothTetheringEnabled()); + } + + + @Test + public void updateBluetoothTetheringState_btPanDeviceNotFound_returnFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device not found"); + return 1; + } + }); + + TetheringObservable.setBluetoothTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isBluetoothTetheringEnabled()); + } + + @Test + public void updateBluetoothTetheringState_ThrowsException_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))). + thenThrow(new SecurityException("Creation of subprocess is not allowed")); + + TetheringObservable.setBluetoothTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isBluetoothTetheringEnabled()); + } + + @Test + public void updateBluetoothTetheringState_WifiManagerWrapperThrowsException_hasNoInfluenceOnResult() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device found"); + return 0; + } + }); + + TetheringObservable.setBluetoothTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isBluetoothTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_ignoreFailingWifiAPReflection_keepsOldValueTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_ignoreFailingWifiAPReflection_keepsOldValueFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_WifiApReflectionWithoutException_changeValueToTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_WifiApReflectionWithoutException_changeValueToFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isWifiTetheringEnabled()); + } + + +}
\ No newline at end of file |