Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1247fd87 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Create a TetherEnabler class to manage tether settings switch"

parents abfd8cfa 23e8fa10
Loading
Loading
Loading
Loading
+254 −0
Original line number Diff line number Diff line
/*
 * 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<BluetoothPan> mBluetoothPan;
    private final SharedPreferences mSharedPreferences;
    private boolean mBluetoothEnableForTether;
    private final BluetoothAdapter mBluetoothAdapter;

    TetherEnabler(Context context, SwitchWidgetController switchWidgetController,
            AtomicReference<BluetoothPan> 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<String> 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
    }
}
+168 −0
Original line number Diff line number Diff line
/*
 * 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<BluetoothPan> 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());
    }
}