Merge "Flow-based connectivity listener" into tm-qpr-dev

This commit is contained in:
TreeHugger Robot
2022-08-02 15:46:34 +00:00
committed by Android (Google) Code Review
6 changed files with 524 additions and 0 deletions

View File

@@ -255,6 +255,16 @@ public class LogModule {
return factory.create("MediaCarouselCtlrLog", 20);
}
/**
* Provides a {@link LogBuffer} for use in the status bar connectivity pipeline
*/
@Provides
@SysUISingleton
@StatusBarConnectivityLog
public static LogBuffer provideStatusBarConnectivityBuffer(LogBufferFactory factory) {
return factory.create("StatusBarConnectivityLog", 64);
}
/** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
@Provides
@SysUISingleton

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 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.systemui.log.dagger;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Qualifier;
/**
* A {@link LogBuffer} for events processed by {@link ConnectivityInfoProcessor}
*/
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface StatusBarConnectivityLog {
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.pipeline
import android.net.Network
import android.net.NetworkCapabilities
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.StatusBarConnectivityLog
import javax.inject.Inject
@SysUISingleton
class ConnectivityPipelineLogger @Inject constructor(
@StatusBarConnectivityLog private val buffer: LogBuffer,
) {
fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
buffer.log(
TAG,
LogLevel.INFO,
{
int1 = network.getNetId()
str1 = networkCapabilities.toString()
},
{
"onCapabilitiesChanged: net=$int1 capabilities=$str1"
}
)
}
fun logOnLost(network: Network) {
buffer.log(
TAG,
LogLevel.INFO,
{
int1 = network.getNetId()
},
{
"onLost: net=$int1"
}
)
}
}
private const val TAG = "SbConnectivityPipeline"

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2021 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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.statusbar.pipeline.repository
import android.annotation.SuppressLint
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.stateIn
/** Repository that contains all relevant [NetworkCapabilites] for the current networks */
@SysUISingleton
class NetworkCapabilitiesRepo @Inject constructor(
connectivityManager: ConnectivityManager,
@Application scope: CoroutineScope,
logger: ConnectivityPipelineLogger,
) {
@SuppressLint("MissingPermission")
val dataStream: StateFlow<Map<Int, NetworkCapabilityInfo>> = run {
var state = emptyMap<Int, NetworkCapabilityInfo>()
callbackFlow {
val networkRequest: NetworkRequest =
NetworkRequest.Builder()
.clearCapabilities()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
val callback =
// TODO (b/240569788): log these using [LogBuffer]
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
logger.logOnCapabilitiesChanged(network, networkCapabilities)
state =
state.toMutableMap().also {
it[network.getNetId()] =
NetworkCapabilityInfo(network, networkCapabilities)
}
trySend(state)
}
override fun onLost(network: Network) {
logger.logOnLost(network)
state = state.toMutableMap().also { it.remove(network.getNetId()) }
trySend(state)
}
}
connectivityManager.registerNetworkCallback(networkRequest, callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.stateIn(scope, started = Lazily, initialValue = state)
}
}
/** contains info about network capabilities. */
data class NetworkCapabilityInfo(
val network: Network,
val capabilities: NetworkCapabilities,
)
private const val TAG = "ConnectivityRepository"

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.pipeline
import android.net.Network
import android.net.NetworkCapabilities
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.log.LogcatEchoTracker
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.mock
@SmallTest
class ConnectivityPipelineLoggerTest : SysuiTestCase() {
private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
.create("buffer", 10)
private val logger = ConnectivityPipelineLogger(buffer)
@Test
fun testLogNetworkCapsChange_bufferHasInfo() {
logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
val expectedNetId = NET_1_ID.toString()
val expectedCaps = NET_1_CAPS.toString()
assertThat(actualString).contains(expectedNetId)
assertThat(actualString).contains(expectedCaps)
}
@Test
fun testLogOnLost_bufferHasNetIdOfLostNetwork() {
logger.logOnLost(NET_1)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
val expectedNetId = NET_1_ID.toString()
assertThat(actualString).contains(expectedNetId)
}
private val NET_1_ID = 100
private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also {
Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID)
}
private val NET_1_CAPS = NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.pipeline.repository
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
// TODO(b/240619365): Update this test to use `runTest` when we update the testing library
@SmallTest
@RunWith(AndroidTestingRunner::class)
class NetworkCapabilitiesRepoTest : SysuiTestCase() {
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun testOnCapabilitiesChanged_oneNewNetwork_networkStored() = runBlocking {
// GIVEN a repo hooked up to [ConnectivityManager]
val scope = CoroutineScope(Dispatchers.Unconfined)
val repo = NetworkCapabilitiesRepo(
connectivityManager = connectivityManager,
scope = scope,
logger = logger,
)
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.dataStream.collect {
}
}
val callback: NetworkCallback = withArgCaptor {
verify(connectivityManager)
.registerNetworkCallback(any(NetworkRequest::class.java), capture())
}
// WHEN a new network is added
callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
val currentMap = repo.dataStream.value
// THEN it is emitted from the flow
assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
job.cancel()
scope.cancel()
}
@Test
fun testOnCapabilitiesChanged_twoNewNetworks_bothStored() = runBlocking {
// GIVEN a repo hooked up to [ConnectivityManager]
val scope = CoroutineScope(Dispatchers.Unconfined)
val repo = NetworkCapabilitiesRepo(
connectivityManager = connectivityManager,
scope = scope,
logger = logger,
)
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.dataStream.collect {
}
}
val callback: NetworkCallback = withArgCaptor {
verify(connectivityManager)
.registerNetworkCallback(any(NetworkRequest::class.java), capture())
}
// WHEN two new networks are added
callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
val currentMap = repo.dataStream.value
// THEN the current state of the flow reflects 2 networks
assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
job.cancel()
scope.cancel()
}
@Test
fun testOnCapabilitesChanged_newCapabilitiesForExistingNetwork_areCaptured() = runBlocking {
// GIVEN a repo hooked up to [ConnectivityManager]
val scope = CoroutineScope(Dispatchers.Unconfined)
val repo = NetworkCapabilitiesRepo(
connectivityManager = connectivityManager,
scope = scope,
logger = logger,
)
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.dataStream.collect {
}
}
val callback: NetworkCallback = withArgCaptor {
verify(connectivityManager)
.registerNetworkCallback(any(NetworkRequest::class.java), capture())
}
// WHEN a network is added, and then its capabilities are changed
callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
callback.onCapabilitiesChanged(NET_1, NET_2_CAPS)
val currentMap = repo.dataStream.value
// THEN the current state of the flow reflects the new capabilities
assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_2_CAPS)
job.cancel()
scope.cancel()
}
@Test
fun testOnLost_networkIsRemoved() = runBlocking {
// GIVEN a repo hooked up to [ConnectivityManager]
val scope = CoroutineScope(Dispatchers.Unconfined)
val repo = NetworkCapabilitiesRepo(
connectivityManager = connectivityManager,
scope = scope,
logger = logger,
)
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.dataStream.collect {
}
}
val callback: NetworkCallback = withArgCaptor {
verify(connectivityManager)
.registerNetworkCallback(any(NetworkRequest::class.java), capture())
}
// WHEN two new networks are added, and one is removed
callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
callback.onLost(NET_1)
val currentMap = repo.dataStream.value
// THEN the current state of the flow reflects only the remaining network
assertThat(currentMap[NET_1_ID]).isNull()
assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
job.cancel()
scope.cancel()
}
@Test
fun testOnLost_noNetworks_doesNotCrash() = runBlocking {
// GIVEN a repo hooked up to [ConnectivityManager]
val scope = CoroutineScope(Dispatchers.Unconfined)
val repo = NetworkCapabilitiesRepo(
connectivityManager = connectivityManager,
scope = scope,
logger = logger,
)
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.dataStream.collect {
}
}
val callback: NetworkCallback = withArgCaptor {
verify(connectivityManager)
.registerNetworkCallback(any(NetworkRequest::class.java), capture())
}
// WHEN no networks are added, and one is removed
callback.onLost(NET_1)
val currentMap = repo.dataStream.value
// THEN the current state of the flow shows no networks
assertThat(currentMap).isEmpty()
job.cancel()
scope.cancel()
}
private val NET_1_ID = 100
private val NET_1 = mock<Network>().also {
whenever(it.getNetId()).thenReturn(NET_1_ID)
}
private val NET_2_ID = 200
private val NET_2 = mock<Network>().also {
whenever(it.getNetId()).thenReturn(NET_2_ID)
}
private val NET_1_CAPS = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.addCapability(NET_CAPABILITY_VALIDATED)
.build()
private val NET_2_CAPS = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_WIFI)
.addCapability(NET_CAPABILITY_NOT_METERED)
.addCapability(NET_CAPABILITY_VALIDATED)
.build()
}