From 23e8fa1020b060ae968a5ddbe1d6363c022bc98d Mon Sep 17 00:00:00 2001 From: Zhen Zhang Date: Tue, 10 Dec 2019 14:52:42 -0800 Subject: [PATCH] Create a TetherEnabler class to manage tether settings switch This class is created to manage the switch state of overall tethering state. It can turn on/off each type of tethering based on stored value in SharedPreference. Also, it listens to data saver state change. Bug: 145923107 Test: TetherEnablerTest Change-Id: I7f360329569f53f34cf13065aa0e00ad9b55f659 --- .../settings/network/TetherEnabler.java | 254 ++++++++++++++++++ .../settings/network/TetherEnablerTest.java | 168 ++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 src/com/android/settings/network/TetherEnabler.java create mode 100644 tests/robotests/src/com/android/settings/network/TetherEnablerTest.java diff --git a/src/com/android/settings/network/TetherEnabler.java b/src/com/android/settings/network/TetherEnabler.java new file mode 100644 index 00000000000..9106aa17fcb --- /dev/null +++ b/src/com/android/settings/network/TetherEnabler.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network; + +import static android.net.ConnectivityManager.TETHERING_BLUETOOTH; +import static android.net.ConnectivityManager.TETHERING_USB; +import static android.net.ConnectivityManager.TETHERING_WIFI; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.datausage.DataSaverBackend; +import com.android.settings.widget.SwitchWidgetController; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; + +/** + * TetherEnabler is a helper to manage Tethering switch on/off state. It turns on/off + * different types of tethering based on stored values in {@link SharedPreferences} and ensures + * tethering state updated by data saver state. + */ + +public final class TetherEnabler implements SwitchWidgetController.OnSwitchChangeListener, + DataSaverBackend.Listener, LifecycleObserver { + @VisibleForTesting + static final String WIFI_TETHER_KEY = "enable_wifi_tethering"; + @VisibleForTesting + static final String USB_TETHER_KEY = "enable_usb_tethering"; + @VisibleForTesting + static final String BLUETOOTH_TETHER_KEY = "enable_bluetooth_tethering"; + + private final SwitchWidgetController mSwitchWidgetController; + private final WifiManager mWifiManager; + private final ConnectivityManager mConnectivityManager; + + private final DataSaverBackend mDataSaverBackend; + private boolean mDataSaverEnabled; + + private final Context mContext; + + @VisibleForTesting + final ConnectivityManager.OnStartTetheringCallback mOnStartTetheringCallback = + new ConnectivityManager.OnStartTetheringCallback() { + @Override + public void onTetheringFailed() { + super.onTetheringFailed(); + mSwitchWidgetController.setChecked(false); + setSwitchWidgetEnabled(true); + } + }; + private final AtomicReference mBluetoothPan; + private final SharedPreferences mSharedPreferences; + private boolean mBluetoothEnableForTether; + private final BluetoothAdapter mBluetoothAdapter; + + TetherEnabler(Context context, SwitchWidgetController switchWidgetController, + AtomicReference bluetoothPan) { + mContext = context; + mSwitchWidgetController = switchWidgetController; + mDataSaverBackend = new DataSaverBackend(context); + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + mConnectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothPan = bluetoothPan; + mDataSaverEnabled = mDataSaverBackend.isDataSaverEnabled(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + mDataSaverBackend.addListener(this); + mSwitchWidgetController.setListener(this); + mSwitchWidgetController.startListening(); + IntentFilter filter = new IntentFilter(ConnectivityManager.ACTION_TETHER_STATE_CHANGED); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiver(mTetherChangeReceiver, filter); + mSwitchWidgetController.setChecked(isTethering()); + setSwitchWidgetEnabled(true); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + mDataSaverBackend.remListener(this); + mSwitchWidgetController.stopListening(); + mContext.unregisterReceiver(mTetherChangeReceiver); + } + + private void setSwitchWidgetEnabled(boolean enabled) { + mSwitchWidgetController.setEnabled(enabled && !mDataSaverEnabled); + } + + private boolean isTethering() { + String[] tethered = mConnectivityManager.getTetheredIfaces(); + return isTethering(tethered); + } + + private boolean isTethering(String[] tethered) { + if (tethered != null && tethered.length != 0) { + return true; + } + + final BluetoothPan pan = mBluetoothPan.get(); + + return pan != null && pan.isTetheringOn(); + } + + @Override + public boolean onSwitchToggled(boolean isChecked) { + if (isChecked) { + startTether(); + } else { + stopTether(); + } + return true; + } + + @VisibleForTesting + void stopTether() { + setSwitchWidgetEnabled(false); + + // Wi-Fi tether is selected by default + if (mSharedPreferences.getBoolean(WIFI_TETHER_KEY, true)) { + mConnectivityManager.stopTethering(TETHERING_WIFI); + } + + // USB tether is not selected by default + if (mSharedPreferences.getBoolean(USB_TETHER_KEY, false)) { + mConnectivityManager.stopTethering(TETHERING_USB); + } + + // Bluetooth tether is not selected by default + if (mSharedPreferences.getBoolean(BLUETOOTH_TETHER_KEY, false)) { + mConnectivityManager.stopTethering(TETHERING_BLUETOOTH); + } + } + + @VisibleForTesting + void startTether() { + setSwitchWidgetEnabled(false); + + // Wi-Fi tether is selected by default + if (mSharedPreferences.getBoolean(WIFI_TETHER_KEY, true)) { + startTethering(TETHERING_WIFI); + } + + // USB tether is not selected by default + if (mSharedPreferences.getBoolean(USB_TETHER_KEY, false)) { + startTethering(TETHERING_USB); + } + + // Bluetooth tether is not selected by default + if (mSharedPreferences.getBoolean(BLUETOOTH_TETHER_KEY, false)) { + startTethering(TETHERING_BLUETOOTH); + } + } + + @VisibleForTesting + void startTethering(int choice) { + if (choice == TETHERING_BLUETOOTH) { + // Turn on Bluetooth first. + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { + mBluetoothEnableForTether = true; + mBluetoothAdapter.enable(); + return; + } + } else if (choice == TETHERING_WIFI && mWifiManager.isWifiApEnabled()) { + return; + } + + + mConnectivityManager.startTethering(choice, true /* showProvisioningUi */, + mOnStartTetheringCallback, new Handler(Looper.getMainLooper())); + } + + private final BroadcastReceiver mTetherChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (TextUtils.equals(ConnectivityManager.ACTION_TETHER_STATE_CHANGED, action)) { + ArrayList active = intent.getStringArrayListExtra( + ConnectivityManager.EXTRA_ACTIVE_TETHER); + mSwitchWidgetController.setChecked( + isTethering(active.toArray(new String[active.size()]))); + setSwitchWidgetEnabled(true); + } else if (TextUtils.equals(BluetoothAdapter.ACTION_STATE_CHANGED, action)) { + if (mBluetoothEnableForTether) { + switch (intent + .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { + case BluetoothAdapter.STATE_ON: + startTethering(TETHERING_BLUETOOTH); + mBluetoothEnableForTether = false; + break; + + case BluetoothAdapter.STATE_OFF: + case BluetoothAdapter.ERROR: + mBluetoothEnableForTether = false; + break; + + default: + // ignore transition states + } + } + } + } + }; + + @Override + public void onDataSaverChanged(boolean isDataSaving) { + mDataSaverEnabled = isDataSaving; + setSwitchWidgetEnabled(!isDataSaving); + } + + @Override + public void onWhitelistStatusChanged(int uid, boolean isWhitelisted) { + // we don't care, since we just want to read the value + } + + @Override + public void onBlacklistStatusChanged(int uid, boolean isBlacklisted) { + // we don't care, since we just want to read the value + } +} diff --git a/tests/robotests/src/com/android/settings/network/TetherEnablerTest.java b/tests/robotests/src/com/android/settings/network/TetherEnablerTest.java new file mode 100644 index 00000000000..06f3893602c --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/TetherEnablerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkPolicyManager; +import android.net.wifi.WifiManager; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.SwitchBarController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.util.ReflectionHelpers; + +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(RobolectricTestRunner.class) +public class TetherEnablerTest { + @Mock + private WifiManager mWifiManager; + @Mock + private ConnectivityManager mConnectivityManager; + @Mock + private NetworkPolicyManager mNetworkPolicyManager; + @Mock + private BluetoothPan mBluetoothPan; + @Mock + private SharedPreferences mSharedPreferences; + + private SwitchBar mSwitchBar; + private TetherEnabler mEnabler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = spy(ApplicationProvider.getApplicationContext()); + AtomicReference panReference = spy(AtomicReference.class); + mSwitchBar = new SwitchBar(context); + when(context.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifiManager); + when(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn( + mConnectivityManager); + when(context.getSystemService(Context.NETWORK_POLICY_SERVICE)).thenReturn( + mNetworkPolicyManager); + when(mConnectivityManager.getTetherableIfaces()).thenReturn(new String[0]); + panReference.set(mBluetoothPan); + mEnabler = new TetherEnabler(context, new SwitchBarController(mSwitchBar), panReference); + } + + @Test + public void lifecycle_onStart_setCheckedCorrectly() { + when(mConnectivityManager.getTetheredIfaces()).thenReturn(new String[]{""}); + + mEnabler.onStart(); + assertThat(mSwitchBar.isChecked()).isTrue(); + } + + @Test + public void startTether_fail_resetSwitchBar() { + when(mNetworkPolicyManager.getRestrictBackground()).thenReturn(false); + + mEnabler.startTether(); + mEnabler.mOnStartTetheringCallback.onTetheringFailed(); + + assertThat(mSwitchBar.isChecked()).isFalse(); + assertThat(mSwitchBar.isEnabled()).isTrue(); + } + + @Test + public void onDataSaverChanged_setsEnabledCorrectly() { + assertThat(mSwitchBar.isEnabled()).isTrue(); + + // try to turn data saver on + when(mNetworkPolicyManager.getRestrictBackground()).thenReturn(true); + mEnabler.onDataSaverChanged(true); + assertThat(mSwitchBar.isEnabled()).isFalse(); + + // lets turn data saver off again + when(mNetworkPolicyManager.getRestrictBackground()).thenReturn(false); + mEnabler.onDataSaverChanged(false); + assertThat(mSwitchBar.isEnabled()).isTrue(); + } + + @Test + public void onSwitchToggled_onlyStartsWifiTetherWhenNeeded() { + when(mWifiManager.isWifiApEnabled()).thenReturn(true); + mEnabler.onSwitchToggled(true); + + verify(mConnectivityManager, never()).startTethering(anyInt(), anyBoolean(), any(), any()); + + doReturn(false).when(mWifiManager).isWifiApEnabled(); + mEnabler.onSwitchToggled(true); + + verify(mConnectivityManager, times(1)) + .startTethering(anyInt(), anyBoolean(), any(), any()); + } + + @Test + public void onSwitchToggled_shouldStartUSBTetherWhenSelected() { + SharedPreferences preference = mock(SharedPreferences.class); + ReflectionHelpers.setField(mEnabler, "mSharedPreferences", preference); + when(preference.getBoolean(mEnabler.WIFI_TETHER_KEY, true)).thenReturn(false); + when(preference.getBoolean(mEnabler.USB_TETHER_KEY, false)).thenReturn(true); + when(preference.getBoolean(mEnabler.BLUETOOTH_TETHER_KEY, true)).thenReturn(false); + + mEnabler.startTether(); + verify(mConnectivityManager, times(1)) + .startTethering(eq(ConnectivityManager.TETHERING_USB), anyBoolean(), any(), any()); + verify(mConnectivityManager, never()) + .startTethering(eq(ConnectivityManager.TETHERING_WIFI), anyBoolean(), any(), any()); + verify(mConnectivityManager, never()).startTethering( + eq(ConnectivityManager.TETHERING_BLUETOOTH), anyBoolean(), any(), any()); + } + + @Test + public void startTether_startsBluetoothTetherWhenOff() { + BluetoothAdapter adapter = mock(BluetoothAdapter.class); + ReflectionHelpers.setField(mEnabler, "mBluetoothAdapter", adapter); + when(adapter.getState()).thenReturn(BluetoothAdapter.STATE_OFF); + + mEnabler.startTethering(ConnectivityManager.TETHERING_BLUETOOTH); + verify(adapter, times(1)).enable(); + + when(adapter.getState()).thenReturn(BluetoothAdapter.STATE_ON); + mEnabler.startTethering(ConnectivityManager.TETHERING_BLUETOOTH); + verify(mConnectivityManager, times(1)).startTethering( + eq(ConnectivityManager.TETHERING_BLUETOOTH), anyBoolean(), any(), any()); + } +}