Merge "Avoid ANR of TetherPreferenceController" into main

This commit is contained in:
Chaohui Wang
2023-11-29 07:04:30 +00:00
committed by Android (Google) Code Review
8 changed files with 461 additions and 524 deletions

View File

@@ -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" />

View File

@@ -98,7 +98,6 @@ public class NetworkDashboardFragment extends DashboardFragment implements
final List<AbstractPreferenceController> 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);

View File

@@ -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<BluetoothPan> 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;
}
}
}
}
}

View File

@@ -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>): 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
}
}

View File

@@ -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<Set<Int>> =
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<Set<TetheringInterface>> = callbackFlow {
val callback = object : TetheringManager.TetheringEventCallback {
override fun onTetheredInterfacesChanged(interfaces: Set<TetheringInterface>) {
trySend(interfaces)
}
}
tetheringManager.registerTetheringEventCallback(Dispatchers.Default.asExecutor(), callback)
awaitClose { tetheringManager.unregisterTetheringEventCallback(callback) }
}.conflate().flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
private fun isBluetoothTetheringOnFlow(): Flow<Boolean> =
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)
}

View File

@@ -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<BluetoothPan> 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();
}
}

View File

@@ -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"
}
}

View File

@@ -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<TetheringInterface> = emptySet()
private var tetheringEventCallback: TetheringManager.TetheringEventCallback? = null
private val mockTetheringManager = mock<TetheringManager> {
on { registerTetheringEventCallback(any(), any()) } doAnswer {
tetheringEventCallback = it.arguments[1] as TetheringManager.TetheringEventCallback
tetheringEventCallback?.onTetheredInterfacesChanged(tetheringInterfaces)
}
}
private val mockBluetoothPan = mock<BluetoothPan> {
on { isTetheringOn } doReturn false
}
private val mockBluetoothAdapter = mock<BluetoothAdapter> {
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<BluetoothManager> {
on { adapter } doReturn mockBluetoothAdapter
}
private val context = mock<Context> {
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)
}
}