Browse Source
* Implement the network status indicator. * Add `networkmonitor` feature.test/jme/compound-poc
Jorge Martin Espinosa
1 year ago
committed by
GitHub
33 changed files with 511 additions and 21 deletions
@ -0,0 +1 @@ |
|||||||
|
Add a `NetworkMonitor` component to track the network connection status |
@ -0,0 +1,31 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2022 New Vector Ltd |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
||||||
|
@Suppress("DSL_SCOPE_VIOLATION") |
||||||
|
plugins { |
||||||
|
id("io.element.android-compose-library") |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.features.networkmonitor.api" |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation(libs.coroutines.core) |
||||||
|
implementation(projects.libraries.designsystem) |
||||||
|
implementation(projects.libraries.uiStrings) |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* 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 io.element.android.features.networkmonitor.api |
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow |
||||||
|
|
||||||
|
interface NetworkMonitor { |
||||||
|
val connectivity: Flow<NetworkStatus> |
||||||
|
val currentConnectivityStatus: NetworkStatus |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* 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 io.element.android.features.networkmonitor.api |
||||||
|
|
||||||
|
enum class NetworkStatus { |
||||||
|
Online, |
||||||
|
Offline |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* 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 io.element.android.features.networkmonitor.api.ui |
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility |
||||||
|
import androidx.compose.animation.core.MutableTransitionState |
||||||
|
import androidx.compose.animation.expandVertically |
||||||
|
import androidx.compose.animation.fadeIn |
||||||
|
import androidx.compose.animation.fadeOut |
||||||
|
import androidx.compose.animation.shrinkVertically |
||||||
|
import androidx.compose.foundation.Image |
||||||
|
import androidx.compose.foundation.background |
||||||
|
import androidx.compose.foundation.layout.Arrangement |
||||||
|
import androidx.compose.foundation.layout.Row |
||||||
|
import androidx.compose.foundation.layout.Spacer |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.foundation.layout.padding |
||||||
|
import androidx.compose.foundation.layout.size |
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding |
||||||
|
import androidx.compose.foundation.layout.width |
||||||
|
import androidx.compose.material.Text |
||||||
|
import androidx.compose.material.icons.Icons |
||||||
|
import androidx.compose.material.icons.outlined.WifiOff |
||||||
|
import androidx.compose.material3.MaterialTheme |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.graphics.ColorFilter |
||||||
|
import androidx.compose.ui.res.stringResource |
||||||
|
import androidx.compose.ui.tooling.preview.Preview |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import io.element.android.libraries.designsystem.ElementTextStyles |
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark |
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight |
||||||
|
import io.element.android.libraries.designsystem.theme.LocalColors |
||||||
|
import io.element.android.libraries.ui.strings.R as StringR |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun ConnectivityIndicatorView( |
||||||
|
isOnline: Boolean, |
||||||
|
modifier: Modifier = Modifier |
||||||
|
) { |
||||||
|
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } |
||||||
|
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline } |
||||||
|
|
||||||
|
// Display the network indicator with an animation |
||||||
|
AnimatedVisibility( |
||||||
|
visibleState = isIndicatorVisible, |
||||||
|
enter = fadeIn() + expandVertically(), |
||||||
|
exit = fadeOut() + shrinkVertically(), |
||||||
|
) { |
||||||
|
Indicator(modifier) |
||||||
|
} |
||||||
|
|
||||||
|
// Show missing status bar padding when the indicator is not visible |
||||||
|
AnimatedVisibility( |
||||||
|
visibleState = isStatusBarPaddingVisible, |
||||||
|
enter = fadeIn() + expandVertically(), |
||||||
|
exit = fadeOut() + shrinkVertically(), |
||||||
|
) { |
||||||
|
StatusBarPaddingSpacer(modifier) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
private fun Indicator(modifier: Modifier = Modifier) { |
||||||
|
Row( |
||||||
|
modifier |
||||||
|
.fillMaxWidth() |
||||||
|
.background(LocalColors.current.gray400) |
||||||
|
.statusBarsPadding() |
||||||
|
.padding(vertical = 6.dp), |
||||||
|
horizontalArrangement = Arrangement.Center, |
||||||
|
verticalAlignment = Alignment.Bottom, |
||||||
|
) { |
||||||
|
val tint = MaterialTheme.colorScheme.primary |
||||||
|
Image( |
||||||
|
imageVector = Icons.Outlined.WifiOff, |
||||||
|
contentDescription = null, |
||||||
|
colorFilter = ColorFilter.tint(tint), |
||||||
|
modifier = Modifier.size(16.dp), |
||||||
|
) |
||||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||||
|
Text(text = stringResource(StringR.string.common_offline), style = ElementTextStyles.Regular.bodyMD, color = tint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
private fun StatusBarPaddingSpacer(modifier: Modifier = Modifier) { |
||||||
|
Spacer(modifier = modifier.statusBarsPadding()) |
||||||
|
} |
||||||
|
|
||||||
|
@Preview |
||||||
|
@Composable |
||||||
|
internal fun PreviewLightConnectivityIndicatorView() { |
||||||
|
ElementPreviewLight { |
||||||
|
ConnectivityIndicatorView(isOnline = false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Preview |
||||||
|
@Composable |
||||||
|
internal fun PreviewDarkConnectivityIndicatorView() { |
||||||
|
ElementPreviewDark { |
||||||
|
ConnectivityIndicatorView(isOnline = false) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2022 New Vector Ltd |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
||||||
|
@Suppress("DSL_SCOPE_VIOLATION") |
||||||
|
plugins { |
||||||
|
id("io.element.android-library") |
||||||
|
alias(libs.plugins.anvil) |
||||||
|
} |
||||||
|
|
||||||
|
anvil { |
||||||
|
generateDaggerFactories.set(true) |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.features.networkmonitor.impl" |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
implementation(libs.coroutines.core) |
||||||
|
implementation(libs.dagger) |
||||||
|
implementation(projects.libraries.core) |
||||||
|
implementation(projects.libraries.di) |
||||||
|
api(projects.features.networkmonitor.api) |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
<!-- |
||||||
|
~ Copyright (c) 2023 New Vector Ltd |
||||||
|
~ |
||||||
|
~ 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. |
||||||
|
--> |
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |
||||||
|
</manifest> |
@ -0,0 +1,93 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* 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 io.element.android.features.networkmonitor.impl |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.net.ConnectivityManager |
||||||
|
import android.net.Network |
||||||
|
import android.net.NetworkCapabilities |
||||||
|
import android.net.NetworkRequest |
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor |
||||||
|
import io.element.android.features.networkmonitor.api.NetworkStatus |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.di.ApplicationContext |
||||||
|
import io.element.android.libraries.di.SingleIn |
||||||
|
import kotlinx.coroutines.flow.Flow |
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||||
|
import timber.log.Timber |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
@ContributesBinding(scope = AppScope::class) |
||||||
|
@SingleIn(AppScope::class) |
||||||
|
class NetworkMonitorImpl @Inject constructor( |
||||||
|
@ApplicationContext context: Context |
||||||
|
) : NetworkMonitor { |
||||||
|
|
||||||
|
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) |
||||||
|
|
||||||
|
private val callback = object : ConnectivityManager.NetworkCallback() { |
||||||
|
override fun onAvailable(network: Network) { |
||||||
|
_connectivity.value = connectivityManager.currentConnectionStatus() |
||||||
|
Timber.v("Connectivity status (available): ${connectivityManager.currentConnectionStatus()}") |
||||||
|
} |
||||||
|
|
||||||
|
override fun onLost(network: Network) { |
||||||
|
_connectivity.value = connectivityManager.currentConnectionStatus() |
||||||
|
Timber.v("Connectivity status (lost): ${connectivityManager.currentConnectionStatus()}") |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCapabilitiesChanged( |
||||||
|
network: Network, |
||||||
|
networkCapabilities: NetworkCapabilities |
||||||
|
) { |
||||||
|
_connectivity.value = connectivityManager.currentConnectionStatus() |
||||||
|
Timber.v("Connectivity status (changed): ${connectivityManager.currentConnectionStatus()}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private val _connectivity = MutableStateFlow(NetworkStatus.Online) |
||||||
|
override val connectivity: Flow<NetworkStatus> = _connectivity |
||||||
|
|
||||||
|
override val currentConnectivityStatus: NetworkStatus get() = _connectivity.value |
||||||
|
|
||||||
|
init { |
||||||
|
listenToConnectionChanges() |
||||||
|
} |
||||||
|
|
||||||
|
private fun listenToConnectionChanges() { |
||||||
|
val request = NetworkRequest.Builder() |
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) |
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) |
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
||||||
|
.build() |
||||||
|
connectivityManager.registerNetworkCallback(request, callback) |
||||||
|
|
||||||
|
_connectivity.tryEmit(connectivityManager.currentConnectionStatus()) |
||||||
|
} |
||||||
|
|
||||||
|
private fun ConnectivityManager.currentConnectionStatus(): NetworkStatus { |
||||||
|
val hasInternet = activeNetwork?.let(::getNetworkCapabilities) |
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
||||||
|
?: false |
||||||
|
return if (hasInternet) { |
||||||
|
NetworkStatus.Online |
||||||
|
} else { |
||||||
|
NetworkStatus.Offline |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2022 New Vector Ltd |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
||||||
|
@Suppress("DSL_SCOPE_VIOLATION") |
||||||
|
plugins { |
||||||
|
id("io.element.android-library") |
||||||
|
} |
||||||
|
|
||||||
|
android { |
||||||
|
namespace = "io.element.android.features.networkmonitor.test" |
||||||
|
} |
||||||
|
|
||||||
|
dependencies { |
||||||
|
api(projects.features.networkmonitor.api) |
||||||
|
api(libs.coroutines.core) |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 New Vector Ltd |
||||||
|
* |
||||||
|
* 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 io.element.android.features.networkmonitor.test |
||||||
|
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor |
||||||
|
import io.element.android.features.networkmonitor.api.NetworkStatus |
||||||
|
import kotlinx.coroutines.flow.Flow |
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||||
|
|
||||||
|
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor { |
||||||
|
override val currentConnectivityStatus: NetworkStatus |
||||||
|
get() = _connectivityStatus.value |
||||||
|
|
||||||
|
private val _connectivityStatus: MutableStateFlow<NetworkStatus> = MutableStateFlow(initialStatus) |
||||||
|
override val connectivity: Flow<NetworkStatus> = _connectivityStatus |
||||||
|
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue