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
This commit is contained in:
Chaohui Wang
2024-12-02 11:46:46 +08:00
parent a25c82a346
commit e731f7cce2
2 changed files with 63 additions and 12 deletions

View File

@@ -35,19 +35,35 @@ 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.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 @@ class TetherPreferenceController(context: Context, key: String) :
}
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 @@ class TetherPreferenceController(context: Context, key: String) :
}
}
override fun updateNonIndexableKeys(keys: MutableList<String>) {
if (!TetherUtil.isTetherAvailable(mContext)) {
keys += preferenceKey
}
}
companion object {
@JvmStatic
fun isTetherConfigDisallowed(context: Context?): Boolean =

View File

@@ -18,6 +18,9 @@ package com.android.settings.network
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.R
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 @@ class TetherPreferenceControllerTest {
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 @@ class TetherPreferenceControllerTest {
.startMocking()
ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
preferenceScreen.addPreference(preference)
controller.displayPreference(preferenceScreen)
}
@After
@@ -57,21 +74,30 @@ class TetherPreferenceControllerTest {
}
@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