From 880068d23a6f30d88db040da022d97480dbed49e Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 28 Nov 2023 18:57:04 +0800 Subject: [PATCH] Avoid ANR of TetherPreferenceController Off load the following work from main thread, - Calculate title - Calculate summery This also helps improve the latency. Also migrate to registerTetheringEventCallback() since TetheringManager.ACTION_TETHER_STATE_CHANGED is deprecated. Fix: 311848767 Test: manual - on Network & internet page and turn on / off tethering Test: unit tests Change-Id: I6ee182b41ef51f691ea31938142be1a41faf5573 --- res/xml/network_provider_internet.xml | 1 + .../network/NetworkDashboardFragment.java | 1 - .../network/TetherPreferenceController.java | 311 ------------------ .../network/TetherPreferenceController.kt | 102 ++++++ .../settings/network/TetheredRepository.kt | 102 ++++++ .../TetherPreferenceControllerTest.java | 212 ------------ .../network/TetherPreferenceControllerTest.kt | 110 +++++++ .../network/TetheredRepositoryTest.kt | 146 ++++++++ 8 files changed, 461 insertions(+), 524 deletions(-) delete mode 100644 src/com/android/settings/network/TetherPreferenceController.java create mode 100644 src/com/android/settings/network/TetherPreferenceController.kt create mode 100644 src/com/android/settings/network/TetheredRepository.kt delete mode 100644 tests/robotests/src/com/android/settings/network/TetherPreferenceControllerTest.java create mode 100644 tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/TetheredRepositoryTest.kt diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml index 8e9d45d6d9c..76ef984c403 100644 --- a/res/xml/network_provider_internet.xml +++ b/res/xml/network_provider_internet.xml @@ -65,6 +65,7 @@ android:icon="@drawable/ic_wifi_tethering" android:order="5" android:summary="@string/summary_placeholder" + settings:controller="com.android.settings.network.TetherPreferenceController" settings:keywords="@string/keywords_hotspot_tethering" settings:userRestriction="no_config_tethering" settings:useAdminDisabledSummary="true" /> diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index 10fdc9886bd..e79c1b29503 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -98,7 +98,6 @@ public class NetworkDashboardFragment extends DashboardFragment implements final List controllers = new ArrayList<>(); controllers.add(new MobileNetworkSummaryController(context, lifecycle, lifecycleOwner)); - controllers.add(new TetherPreferenceController(context, lifecycle)); controllers.add(vpnPreferenceController); if (internetPreferenceController != null) { controllers.add(internetPreferenceController); diff --git a/src/com/android/settings/network/TetherPreferenceController.java b/src/com/android/settings/network/TetherPreferenceController.java deleted file mode 100644 index f9e5a432fd0..00000000000 --- a/src/com/android/settings/network/TetherPreferenceController.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (C) 2016 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.os.UserManager.DISALLOW_CONFIG_TETHERING; - -import static com.android.settingslib.RestrictedLockUtilsInternal.checkIfRestrictionEnforced; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothPan; -import android.bluetooth.BluetoothProfile; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.ContentObserver; -import android.net.TetheringManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.UserHandle; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.FeatureFlagUtils; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settings.core.FeatureFlags; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.TetherUtil; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnCreate; -import com.android.settingslib.core.lifecycle.events.OnDestroy; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; - -import java.util.concurrent.atomic.AtomicReference; - -public class TetherPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin, LifecycleObserver, OnCreate, OnResume, OnPause, OnDestroy { - - private static final String TAG = "TetherPreferenceController"; - private static final String KEY_TETHER_SETTINGS = "tether_settings"; - - private final boolean mAdminDisallowedTetherConfig; - private final AtomicReference mBluetoothPan; - private final BluetoothAdapter mBluetoothAdapter; - private final TetheringManager mTetheringManager; - @VisibleForTesting - final BluetoothProfile.ServiceListener mBtProfileServiceListener = - new android.bluetooth.BluetoothProfile.ServiceListener() { - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (mBluetoothPan.get() == null) { - mBluetoothPan.set((BluetoothPan) proxy); - } - updateSummary(); - } - - public void onServiceDisconnected(int profile) { - updateSummary(); - } - }; - - private SettingObserver mAirplaneModeObserver; - private Preference mPreference; - private TetherBroadcastReceiver mTetherReceiver; - private BroadcastReceiver mBluetoothStateReceiver; - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - TetherPreferenceController() { - super(null); - mAdminDisallowedTetherConfig = false; - mBluetoothPan = new AtomicReference<>(); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - mTetheringManager = null; - } - - public TetherPreferenceController(Context context, Lifecycle lifecycle) { - super(context); - mBluetoothPan = new AtomicReference<>(); - mAdminDisallowedTetherConfig = isTetherConfigDisallowed(context); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - mTetheringManager = context.getSystemService(TetheringManager.class); - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(KEY_TETHER_SETTINGS); - if (mPreference != null && !mAdminDisallowedTetherConfig) { - mPreference.setTitle( - com.android.settingslib.Utils.getTetheringLabel(mTetheringManager)); - } - } - - @Override - public boolean isAvailable() { - return TetherUtil.isTetherAvailable(mContext) - && !FeatureFlagUtils.isEnabled(mContext, FeatureFlags.TETHER_ALL_IN_ONE); - } - - @Override - public void updateState(Preference preference) { - updateSummary(); - } - - @Override - public String getPreferenceKey() { - return KEY_TETHER_SETTINGS; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - if (mBluetoothAdapter != null && - mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { - mBluetoothAdapter.getProfileProxy(mContext, mBtProfileServiceListener, - BluetoothProfile.PAN); - } - if (mBluetoothStateReceiver == null) { - mBluetoothStateReceiver = new BluetoothStateReceiver(); - mContext.registerReceiver( - mBluetoothStateReceiver, - new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - } - } - - @Override - public void onResume() { - if (mAirplaneModeObserver == null) { - mAirplaneModeObserver = new SettingObserver(); - } - if (mTetherReceiver == null) { - mTetherReceiver = new TetherBroadcastReceiver(); - } - mContext.registerReceiver( - mTetherReceiver, new IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)); - mContext.getContentResolver() - .registerContentObserver(mAirplaneModeObserver.uri, false, mAirplaneModeObserver); - } - - @Override - public void onPause() { - if (mAirplaneModeObserver != null) { - mContext.getContentResolver().unregisterContentObserver(mAirplaneModeObserver); - } - if (mTetherReceiver != null) { - mContext.unregisterReceiver(mTetherReceiver); - } - } - - @Override - public void onDestroy() { - final BluetoothProfile profile = mBluetoothPan.getAndSet(null); - if (profile != null && mBluetoothAdapter != null) { - mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PAN, profile); - } - if (mBluetoothStateReceiver != null) { - mContext.unregisterReceiver(mBluetoothStateReceiver); - mBluetoothStateReceiver = null; - } - } - - public static boolean isTetherConfigDisallowed(Context context) { - return checkIfRestrictionEnforced( - context, DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()) != null; - } - - @VisibleForTesting - void updateSummary() { - if (mPreference == null) { - // Preference is not ready yet. - return; - } - String[] allTethered = mTetheringManager.getTetheredIfaces(); - String[] wifiTetherRegex = mTetheringManager.getTetherableWifiRegexs(); - String[] bluetoothRegex = mTetheringManager.getTetherableBluetoothRegexs(); - - boolean hotSpotOn = false; - boolean tetherOn = false; - if (allTethered != null) { - if (wifiTetherRegex != null) { - for (String tethered : allTethered) { - for (String regex : wifiTetherRegex) { - if (tethered.matches(regex)) { - hotSpotOn = true; - break; - } - } - } - } - if (allTethered.length > 1) { - // We have more than 1 tethered connection - tetherOn = true; - } else if (allTethered.length == 1) { - // We have more than 1 tethered, it's either wifiTether (hotspot), or other type of - // tether. - tetherOn = !hotSpotOn; - } else { - // No tethered connection. - tetherOn = false; - } - } - if (!tetherOn - && bluetoothRegex != null && bluetoothRegex.length > 0 - && mBluetoothAdapter != null - && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { - // Check bluetooth state. It's not included in mTetheringManager.getTetheredIfaces. - final BluetoothPan pan = mBluetoothPan.get(); - tetherOn = pan != null && pan.isTetheringOn(); - } - if (!hotSpotOn && !tetherOn) { - // Both off - updateSummaryToOff(); - } else if (hotSpotOn && tetherOn) { - // Both on - mPreference.setSummary(R.string.tether_settings_summary_hotspot_on_tether_on); - } else if (hotSpotOn) { - mPreference.setSummary(R.string.tether_settings_summary_hotspot_on_tether_off); - } else { - mPreference.setSummary(R.string.tether_settings_summary_hotspot_off_tether_on); - } - } - - private void updateSummaryToOff() { - if (mPreference == null) { - // Preference is not ready yet. - return; - } - mPreference.setSummary(R.string.tether_preference_summary_off); - } - - class SettingObserver extends ContentObserver { - - public final Uri uri; - - public SettingObserver() { - super(new Handler()); - uri = Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - if (this.uri.equals(uri)) { - boolean isAirplaneMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) != 0; - if (isAirplaneMode) { - // Airplane mode is on. Update summary to say tether is OFF directly. We cannot - // go through updateSummary() because turning off tether takes time, and we - // might still get "ON" status when rerun updateSummary(). So, just say it's off - updateSummaryToOff(); - } - } - } - } - - @VisibleForTesting - class TetherBroadcastReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - updateSummary(); - } - - } - - private class BluetoothStateReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - Log.i(TAG, "onReceive: action: " + action); - - if (TextUtils.equals(action, BluetoothAdapter.ACTION_STATE_CHANGED)) { - final int state = - intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - Log.i(TAG, "onReceive: state: " + BluetoothAdapter.nameForState(state)); - final BluetoothProfile profile = mBluetoothPan.get(); - switch(state) { - case BluetoothAdapter.STATE_ON: - if (profile == null && mBluetoothAdapter != null) { - mBluetoothAdapter.getProfileProxy(mContext, mBtProfileServiceListener, - BluetoothProfile.PAN); - } - break; - } - } - } - } -} diff --git a/src/com/android/settings/network/TetherPreferenceController.kt b/src/com/android/settings/network/TetherPreferenceController.kt new file mode 100644 index 00000000000..8f55f509117 --- /dev/null +++ b/src/com/android/settings/network/TetherPreferenceController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 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 android.content.Context +import android.net.TetheringManager +import android.os.UserHandle +import android.os.UserManager +import android.util.FeatureFlagUtils +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.core.FeatureFlags +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.settingslib.TetherUtil +import com.android.settingslib.Utils +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class TetherPreferenceController(context: Context, key: String) : + BasePreferenceController(context, key) { + + private val tetheredRepository = TetheredRepository(context) + private val tetheringManager = mContext.getSystemService(TetheringManager::class.java)!! + + private var preference: Preference? = null + + override fun getAvailabilityStatus() = + if (TetherUtil.isTetherAvailable(mContext) + && !FeatureFlagUtils.isEnabled(mContext, FeatureFlags.TETHER_ALL_IN_ONE) + ) { + AVAILABLE + } else { + CONDITIONALLY_UNAVAILABLE + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + getTitleResId()?.let { preference?.setTitle(it) } + } + } + + tetheredRepository.tetheredTypesFlow().collectLatestWithLifecycle(viewLifecycleOwner) { + preference?.setSummary(getSummaryResId(it)) + } + } + + private suspend fun getTitleResId(): Int? = withContext(Dispatchers.Default) { + if (isTetherConfigDisallowed(mContext)) null + else Utils.getTetheringLabel(tetheringManager) + } + + @VisibleForTesting + @StringRes + fun getSummaryResId(tetheredTypes: Set): Int { + val hotSpotOn = TetheringManager.TETHERING_WIFI in tetheredTypes + val tetherOn = tetheredTypes.any { it != TetheringManager.TETHERING_WIFI } + return when { + hotSpotOn && tetherOn -> R.string.tether_settings_summary_hotspot_on_tether_on + hotSpotOn -> R.string.tether_settings_summary_hotspot_on_tether_off + tetherOn -> R.string.tether_settings_summary_hotspot_off_tether_on + else -> R.string.tether_preference_summary_off + } + } + + companion object { + @JvmStatic + fun isTetherConfigDisallowed(context: Context?): Boolean = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId() + ) != null + } +} diff --git a/src/com/android/settings/network/TetheredRepository.kt b/src/com/android/settings/network/TetheredRepository.kt new file mode 100644 index 00000000000..f18bdca47f9 --- /dev/null +++ b/src/com/android/settings/network/TetheredRepository.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 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 android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothPan +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.content.IntentFilter +import android.net.TetheringInterface +import android.net.TetheringManager +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch + +class TetheredRepository(private val context: Context) { + private val tetheringManager = context.getSystemService(TetheringManager::class.java)!! + + private val adapter = context.getSystemService(BluetoothManager::class.java)!!.adapter + + fun tetheredTypesFlow(): Flow> = + combine( + tetheredInterfacesFlow(), + isBluetoothTetheringOnFlow(), + ) { tetheringInterfaces, isBluetoothTetheringOn -> + val mutableSet = tetheringInterfaces.map { it.type }.toMutableSet() + if (isBluetoothTetheringOn) mutableSet += TetheringManager.TETHERING_BLUETOOTH + mutableSet + }.conflate().flowOn(Dispatchers.Default) + + private fun tetheredInterfacesFlow(): Flow> = callbackFlow { + val callback = object : TetheringManager.TetheringEventCallback { + override fun onTetheredInterfacesChanged(interfaces: Set) { + trySend(interfaces) + } + } + + tetheringManager.registerTetheringEventCallback(Dispatchers.Default.asExecutor(), callback) + + awaitClose { tetheringManager.unregisterTetheringEventCallback(callback) } + }.conflate().flowOn(Dispatchers.Default) + + @OptIn(ExperimentalCoroutinesApi::class) + private fun isBluetoothTetheringOnFlow(): Flow = + merge( + flowOf(null), // kick an initial value + context.broadcastReceiverFlow(IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)), + ).flatMapLatest { + if (adapter.getState() == BluetoothAdapter.STATE_ON) { + isBluetoothPanTetheringOnFlow() + } else { + flowOf(false) + } + }.conflate().flowOn(Dispatchers.Default) + + private fun isBluetoothPanTetheringOnFlow() = callbackFlow { + var connectedProxy: BluetoothProfile? = null + + val listener = object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + connectedProxy = proxy + launch(Dispatchers.Default) { + trySend((proxy as BluetoothPan).isTetheringOn) + } + } + + override fun onServiceDisconnected(profile: Int) {} + } + + adapter.getProfileProxy(context, listener, BluetoothProfile.PAN) + + awaitClose { + connectedProxy?.let { adapter.closeProfileProxy(BluetoothProfile.PAN, it) } + } + }.conflate().flowOn(Dispatchers.Default) +} diff --git a/tests/robotests/src/com/android/settings/network/TetherPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/TetherPreferenceControllerTest.java deleted file mode 100644 index 99869d85429..00000000000 --- a/tests/robotests/src/com/android/settings/network/TetherPreferenceControllerTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2017 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 org.mockito.Mockito.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothPan; -import android.bluetooth.BluetoothProfile; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.ContentObserver; -import android.net.TetheringManager; -import android.provider.Settings; - -import androidx.preference.Preference; - -import com.android.settings.R; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.util.ReflectionHelpers; - -import java.util.concurrent.atomic.AtomicReference; - -@RunWith(RobolectricTestRunner.class) -public class TetherPreferenceControllerTest { - - @Mock - private Context mContext; - @Mock - private TetheringManager mTetheringManager; - @Mock - private BluetoothAdapter mBluetoothAdapter; - @Mock - private Preference mPreference; - - private TetherPreferenceController mController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - doReturn(null).when(mContext) - .getSystemService(Context.DEVICE_POLICY_SERVICE); - mController = spy(new TetherPreferenceController(mContext, /* lifecycle= */ null)); - ReflectionHelpers.setField(mController, "mContext", mContext); - ReflectionHelpers.setField(mController, "mTetheringManager", mTetheringManager); - ReflectionHelpers.setField(mController, "mBluetoothAdapter", mBluetoothAdapter); - ReflectionHelpers.setField(mController, "mPreference", mPreference); - } - - @Test - public void lifeCycle_onCreate_shouldInitBluetoothPan() { - when(mBluetoothAdapter.getState()).thenReturn(BluetoothAdapter.STATE_ON); - mController.onCreate(null); - - verify(mBluetoothAdapter).getState(); - verify(mBluetoothAdapter).getProfileProxy(mContext, mController.mBtProfileServiceListener, - BluetoothProfile.PAN); - } - - @Test - public void lifeCycle_onCreate_shouldNotInitBluetoothPanWhenBluetoothOff() { - when(mBluetoothAdapter.getState()).thenReturn(BluetoothAdapter.STATE_OFF); - mController.onCreate(null); - - verify(mBluetoothAdapter).getState(); - verifyNoMoreInteractions(mBluetoothAdapter); - } - - @Test - public void goThroughLifecycle_shouldDestoryBluetoothProfile() { - final BluetoothPan pan = mock(BluetoothPan.class); - final AtomicReference panRef = - ReflectionHelpers.getField(mController, "mBluetoothPan"); - panRef.set(pan); - - mController.onDestroy(); - - verify(mBluetoothAdapter).closeProfileProxy(BluetoothProfile.PAN, pan); - } - - @Test - public void updateSummary_noPreference_noInteractionWithTetheringManager() { - ReflectionHelpers.setField(mController, "mPreference", null); - mController.updateSummary(); - verifyNoMoreInteractions(mTetheringManager); - } - - @Test - public void updateSummary_wifiTethered_shouldShowHotspotMessage() { - when(mTetheringManager.getTetheredIfaces()).thenReturn(new String[]{"123"}); - when(mTetheringManager.getTetherableWifiRegexs()).thenReturn(new String[]{"123"}); - - mController.updateSummary(); - verify(mPreference).setSummary(R.string.tether_settings_summary_hotspot_on_tether_off); - } - - @Test - public void updateSummary_btThetherOn_shouldShowTetherMessage() { - when(mTetheringManager.getTetheredIfaces()).thenReturn(new String[]{"123"}); - when(mTetheringManager.getTetherableBluetoothRegexs()).thenReturn(new String[]{"123"}); - - mController.updateSummary(); - verify(mPreference).setSummary(R.string.tether_settings_summary_hotspot_off_tether_on); - } - - @Ignore - @Test - public void updateSummary_tetherOff_shouldShowTetherOffMessage() { - when(mTetheringManager.getTetherableBluetoothRegexs()).thenReturn(new String[]{"123"}); - when(mTetheringManager.getTetherableWifiRegexs()).thenReturn(new String[]{"456"}); - - mController.updateSummary(); - verify(mPreference).setSummary(R.string.switch_off_text); - } - - @Test - public void updateSummary_wifiBtTetherOn_shouldShowHotspotAndTetherMessage() { - when(mTetheringManager.getTetheredIfaces()).thenReturn(new String[]{"123", "456"}); - when(mTetheringManager.getTetherableWifiRegexs()).thenReturn(new String[]{"456"}); - when(mTetheringManager.getTetherableBluetoothRegexs()).thenReturn(new String[]{"23"}); - - mController.updateSummary(); - verify(mPreference).setSummary(R.string.tether_settings_summary_hotspot_on_tether_on); - } - - @Ignore - @Test - public void airplaneModeOn_shouldUpdateSummaryToOff() { - final Context context = RuntimeEnvironment.application; - ReflectionHelpers.setField(mController, "mContext", context); - - Settings.Global.putInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - - mController.onResume(); - - verifyNoInteractions(mPreference); - - Settings.Global.putInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - - final ContentObserver observer = - ReflectionHelpers.getField(mController, "mAirplaneModeObserver"); - observer.onChange(true, Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON)); - - verify(mPreference).setSummary(R.string.switch_off_text); - } - - @Test - public void onResume_shouldRegisterTetherReceiver() { - when(mContext.getContentResolver()).thenReturn(mock(ContentResolver.class)); - - mController.onResume(); - - verify(mContext).registerReceiver( - any(TetherPreferenceController.TetherBroadcastReceiver.class), - any(IntentFilter.class)); - } - - @Test - public void onPause_shouldUnregisterTetherReceiver() { - when(mContext.getContentResolver()).thenReturn(mock(ContentResolver.class)); - mController.onResume(); - - mController.onPause(); - - verify(mContext) - .unregisterReceiver(any(TetherPreferenceController.TetherBroadcastReceiver.class)); - } - - @Test - public void tetherStatesChanged_shouldUpdateSummary() { - final Context context = RuntimeEnvironment.application; - ReflectionHelpers.setField(mController, "mContext", context); - mController.onResume(); - - context.sendBroadcast(new Intent(TetheringManager.ACTION_TETHER_STATE_CHANGED)); - - shadowMainLooper().idle(); - verify(mController).updateSummary(); - } -} diff --git a/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt new file mode 100644 index 00000000000..51d2c87dff3 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 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 android.content.Context +import android.net.TetheringManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settingslib.TetherUtil +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoSession +import org.mockito.quality.Strictness + +@RunWith(AndroidJUnit4::class) +class TetherPreferenceControllerTest { + private lateinit var mockSession: MockitoSession + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val controller = TetherPreferenceController(context, TEST_KEY) + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(TetherUtil::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + + ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) } + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + @Test + fun getAvailabilityStatus_whenTetherAvailable() { + ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) } + + val availabilityStatus = controller.availabilityStatus + + assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE) + } + + @Test + fun getAvailabilityStatus_whenTetherNotAvailable() { + ExtendedMockito.doReturn(false).`when` { TetherUtil.isTetherAvailable(context) } + + val availabilityStatus = controller.availabilityStatus + + assertThat(availabilityStatus).isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun getSummaryResId_bothWifiAndBluetoothOn() { + val summaryResId = controller.getSummaryResId( + setOf(TetheringManager.TETHERING_WIFI, TetheringManager.TETHERING_BLUETOOTH) + ) + + assertThat(summaryResId).isEqualTo(R.string.tether_settings_summary_hotspot_on_tether_on) + } + + @Test + fun getSummaryResId_onlyWifiHotspotOn() { + val summaryResId = controller.getSummaryResId(setOf(TetheringManager.TETHERING_WIFI)) + + assertThat(summaryResId).isEqualTo(R.string.tether_settings_summary_hotspot_on_tether_off) + } + + @Test + fun getSummaryResId_onlyBluetoothTetheringOn() { + val summaryResId = controller.getSummaryResId(setOf(TetheringManager.TETHERING_BLUETOOTH)) + + assertThat(summaryResId).isEqualTo(R.string.tether_settings_summary_hotspot_off_tether_on) + } + + @Test + fun getSummaryResId_allOff() { + val summaryResId = controller.getSummaryResId(emptySet()) + + assertThat(summaryResId).isEqualTo(R.string.tether_preference_summary_off) + } + + private companion object { + const val TEST_KEY = "test_key" + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/TetheredRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/TetheredRepositoryTest.kt new file mode 100644 index 00000000000..5bd5210aa91 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/TetheredRepositoryTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 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 android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothPan +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.net.TetheringInterface +import android.net.TetheringManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class TetheredRepositoryTest { + + private var tetheringInterfaces: Set = emptySet() + + private var tetheringEventCallback: TetheringManager.TetheringEventCallback? = null + + private val mockTetheringManager = mock { + on { registerTetheringEventCallback(any(), any()) } doAnswer { + tetheringEventCallback = it.arguments[1] as TetheringManager.TetheringEventCallback + tetheringEventCallback?.onTetheredInterfacesChanged(tetheringInterfaces) + } + } + + private val mockBluetoothPan = mock { + on { isTetheringOn } doReturn false + } + + private val mockBluetoothAdapter = mock { + on { getProfileProxy(any(), any(), eq(BluetoothProfile.PAN)) } doAnswer { + val listener = it.arguments[1] as BluetoothProfile.ServiceListener + listener.onServiceConnected(BluetoothProfile.PAN, mockBluetoothPan) + true + } + } + + private val mockBluetoothManager = mock { + on { adapter } doReturn mockBluetoothAdapter + } + + private val context = mock { + on { getSystemService(TetheringManager::class.java) } doReturn mockTetheringManager + on { getSystemService(BluetoothManager::class.java) } doReturn mockBluetoothManager + } + + private val repository = TetheredRepository(context) + + @Test + fun tetheredTypesFlow_allOff() = runBlocking { + val tetheredTypes = repository.tetheredTypesFlow().firstWithTimeoutOrNull() + + assertThat(tetheredTypes).isEmpty() + } + + @Test + fun tetheredTypesFlow_wifiHotspotOn(): Unit = runBlocking { + tetheringInterfaces = setOf(TetheringInterface(TetheringManager.TETHERING_WIFI, "")) + + val tetheredTypes = repository.tetheredTypesFlow().firstWithTimeoutOrNull() + + assertThat(tetheredTypes).containsExactly(TetheringManager.TETHERING_WIFI) + } + + @Test + fun tetheredTypesFlow_usbTetheringTurnOnLater(): Unit = runBlocking { + val tetheredTypeDeferred = async { + repository.tetheredTypesFlow().mapNotNull { + it.singleOrNull() + }.firstWithTimeoutOrNull() + } + delay(100) + + tetheringEventCallback?.onTetheredInterfacesChanged( + setOf(TetheringInterface(TetheringManager.TETHERING_USB, "")) + ) + + assertThat(tetheredTypeDeferred.await()).isEqualTo(TetheringManager.TETHERING_USB) + } + + @Test + fun tetheredTypesFlow_bluetoothOff(): Unit = runBlocking { + mockBluetoothAdapter.stub { + on { state } doReturn BluetoothAdapter.STATE_OFF + } + + val tetheredTypes = repository.tetheredTypesFlow().firstWithTimeoutOrNull() + + assertThat(tetheredTypes).isEmpty() + } + + @Test + fun tetheredTypesFlow_bluetoothOnTetheringOff(): Unit = runBlocking { + mockBluetoothAdapter.stub { + on { state } doReturn BluetoothAdapter.STATE_ON + } + + val tetheredTypes = repository.tetheredTypesFlow().firstWithTimeoutOrNull() + + assertThat(tetheredTypes).isEmpty() + } + + @Test + fun tetheredTypesFlow_bluetoothTetheringOn(): Unit = runBlocking { + mockBluetoothAdapter.stub { + on { state } doReturn BluetoothAdapter.STATE_ON + } + mockBluetoothPan.stub { + on { isTetheringOn } doReturn true + } + + val tetheredTypes = repository.tetheredTypesFlow().firstWithTimeoutOrNull() + + assertThat(tetheredTypes).containsExactly(TetheringManager.TETHERING_BLUETOOTH) + } +}