/* * 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 static android.net.TetheringManager.TETHERING_ETHERNET; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; 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.net.ConnectivityManager; import android.net.TetheringManager; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.UserManager; import android.text.TextUtils; import android.util.Log; import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.datausage.DataSaverBackend; import com.android.settings.widget.SwitchWidgetController; import java.lang.annotation.Retention; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * TetherEnabler is a helper to manage Tethering switch on/off state. It offers helper functions to * turn on/off different types of tethering interfaces and ensures tethering state updated by data * saver state. * * This class is not designed for extending. It's extendable solely for the test purpose. */ public class TetherEnabler implements SwitchWidgetController.OnSwitchChangeListener, DataSaverBackend.Listener, LifecycleObserver { /** * Interface definition for a callback to be invoked when the tethering has been updated. */ public interface OnTetherStateUpdateListener { /** * Called when the tethering state has changed. * * @param state The new tethering state. */ void onTetherStateUpdated(@TetheringState int state); } private static final String TAG = "TetherEnabler"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @Retention(SOURCE) @IntDef( flag = true, value = { TETHERING_OFF, TETHERING_WIFI_ON, TETHERING_USB_ON, TETHERING_BLUETOOTH_ON, TETHERING_ETHERNET_ON } ) @interface TetheringState {} public static final int TETHERING_OFF = 0; public static final int TETHERING_WIFI_ON = 1 << TETHERING_WIFI; public static final int TETHERING_USB_ON = 1 << TETHERING_USB; public static final int TETHERING_BLUETOOTH_ON = 1 << TETHERING_BLUETOOTH; public static final int TETHERING_ETHERNET_ON = 1 << TETHERING_ETHERNET; @VisibleForTesting final List mListeners; private final Handler mMainThreadHandler; private final SwitchWidgetController mSwitchWidgetController; private final WifiManager mWifiManager; private final ConnectivityManager mConnectivityManager; private final TetheringManager mTetheringManager; private final UserManager mUserManager; private final String mEthernetRegex; private final DataSaverBackend mDataSaverBackend; private boolean mDataSaverEnabled; @VisibleForTesting boolean mBluetoothTetheringStoppedByUser; private final Context mContext; @VisibleForTesting TetheringManager.TetheringEventCallback mTetheringEventCallback; @VisibleForTesting ConnectivityManager.OnStartTetheringCallback mOnStartTetheringCallback; private final AtomicReference mBluetoothPan; private boolean mBluetoothEnableForTether; private final BluetoothAdapter mBluetoothAdapter; public TetherEnabler(Context context, SwitchWidgetController switchWidgetController, AtomicReference bluetoothPan) { mContext = context; mSwitchWidgetController = switchWidgetController; mDataSaverBackend = new DataSaverBackend(context); mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); mTetheringManager = (TetheringManager) context.getSystemService(Context.TETHERING_SERVICE); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mBluetoothPan = bluetoothPan; mEthernetRegex = context.getString(com.android.internal.R.string.config_ethernet_iface_regex); mDataSaverEnabled = mDataSaverBackend.isDataSaverEnabled(); mListeners = new ArrayList<>(); mMainThreadHandler = new Handler(Looper.getMainLooper()); } @OnLifecycleEvent(Lifecycle.Event.ON_START) public void onStart() { mDataSaverBackend.addListener(this); mSwitchWidgetController.setListener(this); mSwitchWidgetController.startListening(); final IntentFilter filter = new IntentFilter( ConnectivityManager.ACTION_TETHER_STATE_CHANGED); filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(mTetherChangeReceiver, filter); mTetheringEventCallback = new TetheringManager.TetheringEventCallback() { @Override public void onTetheredInterfacesChanged(List interfaces) { updateState(interfaces.toArray(new String[interfaces.size()])); } }; mTetheringManager.registerTetheringEventCallback(new HandlerExecutor(mMainThreadHandler), mTetheringEventCallback); mOnStartTetheringCallback = new OnStartTetheringCallback(this); updateState(null/*tethered*/); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void onStop() { mBluetoothTetheringStoppedByUser = false; mDataSaverBackend.remListener(this); mSwitchWidgetController.stopListening(); mContext.unregisterReceiver(mTetherChangeReceiver); mTetheringManager.unregisterTetheringEventCallback(mTetheringEventCallback); mTetheringEventCallback = null; } public void addListener(OnTetherStateUpdateListener listener) { if (listener != null && !mListeners.contains(listener)) { listener.onTetherStateUpdated(getTetheringState(null /* tethered */)); mListeners.add(listener); } } public void removeListener(OnTetherStateUpdateListener listener) { if (listener != null) { mListeners.remove(listener); } } private void setSwitchEnabled(boolean enabled) { mSwitchWidgetController.setEnabled( enabled && !mDataSaverEnabled && mUserManager.isAdminUser()); } @VisibleForTesting void updateState(@Nullable String[] tethered) { int state = getTetheringState(tethered); if (DEBUG) { Log.d(TAG, "updateState: " + state); } setSwitchCheckedInternal(state != TETHERING_OFF); setSwitchEnabled(true); for (int i = 0, size = mListeners.size(); i < size; ++i) { mListeners.get(i).onTetherStateUpdated(state); } } private void setSwitchCheckedInternal(boolean checked) { try { mSwitchWidgetController.stopListening(); } catch (IllegalStateException e) { Log.e(TAG, "failed to stop switch widget listener when set check internally"); return; } mSwitchWidgetController.setChecked(checked); mSwitchWidgetController.startListening(); } @VisibleForTesting @TetheringState int getTetheringState(@Nullable String[] tethered) { int tetherState = TETHERING_OFF; if (tethered == null) { tethered = mConnectivityManager.getTetheredIfaces(); } if (mWifiManager.isWifiApEnabled()) { tetherState |= TETHERING_WIFI_ON; } // Only check bluetooth tethering state if not stopped by user already. if (!mBluetoothTetheringStoppedByUser) { final BluetoothPan pan = mBluetoothPan.get(); if (mBluetoothAdapter != null && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON && pan != null && pan.isTetheringOn()) { tetherState |= TETHERING_BLUETOOTH_ON; } } String[] usbRegexs = mConnectivityManager.getTetherableUsbRegexs(); for (String s : tethered) { for (String regex : usbRegexs) { if (s.matches(regex)) { tetherState |= TETHERING_USB_ON; } } if (s.matches(mEthernetRegex)) { tetherState |= TETHERING_ETHERNET_ON; } } return tetherState; } public static boolean isTethering(@TetheringState int state, int choice) { return (state & (1 << choice)) != TETHERING_OFF; } @Override public boolean onSwitchToggled(boolean isChecked) { if (isChecked) { startTethering(TETHERING_WIFI); } else { stopTethering(TETHERING_USB); stopTethering(TETHERING_WIFI); stopTethering(TETHERING_BLUETOOTH); stopTethering(TETHERING_ETHERNET); } return true; } public void stopTethering(int choice) { int state = getTetheringState(null /* tethered */); if (isTethering(state, choice)) { setSwitchEnabled(false); mConnectivityManager.stopTethering(choice); if (choice == TETHERING_BLUETOOTH) { // Stop bluetooth tether won't invoke tether state changed callback, so we need this // boolean to remember the user action and update UI state immediately. mBluetoothTetheringStoppedByUser = true; updateState(null /* tethered */); } } } public void startTethering(int choice) { if (choice == TETHERING_BLUETOOTH) { mBluetoothTetheringStoppedByUser = false; } int state = getTetheringState(null /* tethered */); if (isTethering(state, choice)) { return; } if (choice == TETHERING_BLUETOOTH && mBluetoothAdapter != null && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { if (DEBUG) { Log.d(TAG, "Turn on bluetooth first."); } mBluetoothEnableForTether = true; mBluetoothAdapter.enable(); return; } setSwitchEnabled(false); mConnectivityManager.startTethering(choice, true /* showProvisioningUi */, mOnStartTetheringCallback, mMainThreadHandler); } private final BroadcastReceiver mTetherChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); boolean shouldUpdateState = false; if (TextUtils.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION, action)) { shouldUpdateState = handleWifiApStateChanged(intent.getIntExtra( WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED)); } else if (TextUtils.equals(BluetoothAdapter.ACTION_STATE_CHANGED, action)) { shouldUpdateState = handleBluetoothStateChanged(intent .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)); } if (shouldUpdateState) { updateState(null /* tethered */); } } }; private boolean handleBluetoothStateChanged(int state) { switch (state) { case BluetoothAdapter.STATE_ON: if (mBluetoothEnableForTether) { startTethering(TETHERING_BLUETOOTH); } // Fall through. case BluetoothAdapter.STATE_OFF: // Fall through. case BluetoothAdapter.ERROR: mBluetoothEnableForTether = false; return true; default: // Return false for transition states. return false; } } private boolean handleWifiApStateChanged(int state) { switch (state) { case WifiManager.WIFI_AP_STATE_FAILED: Log.e(TAG, "Wifi AP is failed!"); // fall through case WifiManager.WIFI_AP_STATE_ENABLED: // fall through case WifiManager.WIFI_AP_STATE_DISABLED: return true; default: // return false for transition state return false; } } @Override public void onDataSaverChanged(boolean isDataSaving) { mDataSaverEnabled = isDataSaving; setSwitchEnabled(true); } @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 } private static final class OnStartTetheringCallback extends ConnectivityManager.OnStartTetheringCallback { final WeakReference mTetherEnabler; OnStartTetheringCallback(TetherEnabler enabler) { mTetherEnabler = new WeakReference<>(enabler); } @Override public void onTetheringStarted() { update(); } @Override public void onTetheringFailed() { update(); } private void update() { TetherEnabler enabler = mTetherEnabler.get(); if (enabler != null) { enabler.updateState(null/*tethered*/); } } } }