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.Utils
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers 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.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class TetherPreferenceController(context: Context, key: String) : class TetherPreferenceController(
BasePreferenceController(context, key) { 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 val tetheringManager = mContext.getSystemService(TetheringManager::class.java)!!
private var preference: Preference? = null private var preference: Preference? = null
override fun getAvailabilityStatus() = private val isTetherAvailableFlow =
if (TetherUtil.isTetherAvailable(mContext)) AVAILABLE else CONDITIONALLY_UNAVAILABLE 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) { override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen) super.displayPreference(screen)
@@ -55,6 +71,9 @@ class TetherPreferenceController(context: Context, key: String) :
} }
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
isTetherAvailableFlow.collectLatestWithLifecycle(viewLifecycleOwner) {
preference?.isVisible = it
}
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
getTitleResId()?.let { preference?.setTitle(it) } 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 { companion object {
@JvmStatic @JvmStatic
fun isTetherConfigDisallowed(context: Context?): Boolean = fun isTetherConfigDisallowed(context: Context?): Boolean =

View File

@@ -18,6 +18,9 @@ package com.android.settings.network
import android.content.Context import android.content.Context
import android.net.TetheringManager 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.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito 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.settings.core.BasePreferenceController
import com.android.settingslib.TetherUtil import com.android.settingslib.TetherUtil
import com.google.common.truth.Truth.assertThat 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.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.MockitoSession import org.mockito.MockitoSession
import org.mockito.kotlin.mock
import org.mockito.quality.Strictness import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -38,7 +45,14 @@ class TetherPreferenceControllerTest {
private val context: Context = ApplicationProvider.getApplicationContext() 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 @Before
fun setUp() { fun setUp() {
@@ -49,6 +63,9 @@ class TetherPreferenceControllerTest {
.startMocking() .startMocking()
ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) } ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
preferenceScreen.addPreference(preference)
controller.displayPreference(preferenceScreen)
} }
@After @After
@@ -57,21 +74,30 @@ class TetherPreferenceControllerTest {
} }
@Test @Test
fun getAvailabilityStatus_whenTetherAvailable() { fun getAvailabilityStatus_alwaysReturnAvailable() {
ExtendedMockito.doReturn(true).`when` { TetherUtil.isTetherAvailable(context) }
val availabilityStatus = controller.availabilityStatus val availabilityStatus = controller.availabilityStatus
assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE) assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE)
} }
@Test @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) } 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 @Test