summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java53
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java2
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java279
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