Fix TetherPreferenceController ANR
getAvailabilityStatus() is called in main thread, so we should avoid
time consuming works in it.
Fix: 377146536
Flag: EXEMPT bug fix
Test: manual - on Network & internet
Test: unit test
Change-Id: Ib5ee19744cf164f91aa90be982f5fc5eead5d4d3
diff --git a/src/com/android/settings/network/TetherPreferenceController.kt b/src/com/android/settings/network/TetherPreferenceController.kt
index c36a2382..524eb78 100644
--- a/src/com/android/settings/network/TetherPreferenceController.kt
+++ b/src/com/android/settings/network/TetherPreferenceController.kt
@@ -35,19 +35,35 @@
import com.android.settingslib.Utils
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-class TetherPreferenceController(context: Context, key: String) :
- BasePreferenceController(context, key) {
+class TetherPreferenceController(
+ context: Context,
+ key: String,
+ private val tetheredRepository: TetheredRepository = TetheredRepository(context),
+) : 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)) AVAILABLE else CONDITIONALLY_UNAVAILABLE
+ private val isTetherAvailableFlow =
+ flow { emit(TetherUtil.isTetherAvailable(mContext)) }
+ .distinctUntilChanged()
+ .conflate()
+ .flowOn(Dispatchers.Default)
+
+ /**
+ * Always returns available here to avoid ANR.
+ * - Actual UI visibility is handled in [onViewCreated].
+ * - Search visibility is handled in [updateNonIndexableKeys].
+ */
+ override fun getAvailabilityStatus() = AVAILABLE
override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen)
@@ -55,6 +71,9 @@
}
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+ isTetherAvailableFlow.collectLatestWithLifecycle(viewLifecycleOwner) {
+ preference?.isVisible = it
+ }
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
getTitleResId()?.let { preference?.setTitle(it) }
@@ -84,6 +103,12 @@
}
}
+ override fun updateNonIndexableKeys(keys: MutableList<String>) {
+ if (!TetherUtil.isTetherAvailable(mContext)) {
+ keys += preferenceKey
+ }
+ }
+
companion object {
@JvmStatic
fun isTetherConfigDisallowed(context: Context?): Boolean =
diff --git a/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt
index 51d2c87..205cfa0 100644
--- a/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/TetherPreferenceControllerTest.kt
@@ -18,6 +18,9 @@
import android.content.Context
import android.net.TetheringManager
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
@@ -25,11 +28,15 @@
import com.android.settings.core.BasePreferenceController
import com.android.settingslib.TetherUtil
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoSession
+import org.mockito.kotlin.mock
import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class)
@@ -38,7 +45,14 @@
private val context: Context = ApplicationProvider.getApplicationContext()
- private val controller = TetherPreferenceController(context, TEST_KEY)
+ private val mockTetheredRepository =
+ mock<TetheredRepository> { on { tetheredTypesFlow() }.thenReturn(flowOf(emptySet())) }
+
+ private val controller = TetherPreferenceController(context, TEST_KEY, mockTetheredRepository)
+
+ private val preference = PreferenceCategory(context).apply { key = TEST_KEY }
+
+ private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
@Before
fun setUp() {
@@ -49,6 +63,9 @@
.startMocking()
ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
+
+ preferenceScreen.addPreference(preference)
+ controller.displayPreference(preferenceScreen)
}
@After
@@ -57,21 +74,30 @@
}
@Test
- fun getAvailabilityStatus_whenTetherAvailable() {
- ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
-
+ fun getAvailabilityStatus_alwaysReturnAvailable() {
val availabilityStatus = controller.availabilityStatus
assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE)
}
@Test
- fun getAvailabilityStatus_whenTetherNotAvailable() {
+ fun onViewCreated_whenTetherAvailable() = runBlocking {
+ ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
+
+ controller.onViewCreated(TestLifecycleOwner())
+ delay(100)
+
+ assertThat(preference.isVisible).isTrue()
+ }
+
+ @Test
+ fun onViewCreated_whenTetherNotAvailable() = runBlocking {
ExtendedMockito.doReturn(false).`when` { TetherUtil.isTetherAvailable(context) }
- val availabilityStatus = controller.availabilityStatus
+ controller.onViewCreated(TestLifecycleOwner())
+ delay(100)
- assertThat(availabilityStatus).isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE)
+ assertThat(preference.isVisible).isFalse()
}
@Test