First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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)
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.networkmonitor.api
import kotlinx.coroutines.flow.StateFlow
/**
* Monitors the network status of the device, providing the current network connectivity status as a flow.
*
* **Note:** network connectivity does not imply internet connectivity. The device can be connected to a network that can't reach the homeserver.
*/
interface NetworkMonitor {
/**
* A flow containing the current network connectivity status.
*/
val connectivity: StateFlow<NetworkStatus>
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.networkmonitor.api
/**
* Network connectivity status of the device.
*
* **Note:** this is *network* connectivity status, not *internet* connectivity status.
*/
enum class NetworkStatus {
/**
* The device is connected to a network.
*/
Connected,
/**
* The device is not connected to any networks.
*/
Disconnected
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.networkmonitor.api.ui
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun ConnectivityIndicator(
verticalPadding: Dp,
modifier: Modifier = Modifier,
) {
Row(
modifier
.fillMaxWidth()
.background(ElementTheme.colors.bgSubtlePrimary)
.statusBarsPadding()
.padding(vertical = verticalPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = CompoundIcons.Offline(),
contentDescription = null,
tint = ElementTheme.colors.iconPrimary,
modifier = Modifier.size(16.sp.toDp()),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(CommonStrings.common_offline),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
}
}
@PreviewsDayNight
@Composable
internal fun ConnectivityIndicatorPreview() = ElementPreview {
ConnectivityIndicator(verticalPadding = 6.dp)
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.networkmonitor.api.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.dp
private val INDICATOR_VERTICAL_PADDING = 6.dp
/**
* A view that displays a connectivity indicator when the device is offline.
*/
@Composable
fun ConnectivityIndicatorContainer(
isOnline: Boolean,
modifier: Modifier = Modifier,
content: @Composable (Modifier) -> Unit = {},
) {
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
Column(modifier = modifier) {
val statusBarTopPadding = if (LocalInspectionMode.current) {
// Needed to get valid UI previews
24.dp
} else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + INDICATOR_VERTICAL_PADDING
}
val target = if (isIndicatorVisible.targetState) statusBarTopPadding else 0.dp
val topWindowInset by animateDpAsState(
targetValue = target,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 1.dp,
),
label = "insets-animation",
)
// Display the network indicator with an animation
AnimatedVisibility(
visibleState = isIndicatorVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ConnectivityIndicator(verticalPadding = INDICATOR_VERTICAL_PADDING)
}
// Consume the window insets to avoid double padding.
content(
Modifier.consumeWindowInsets(PaddingValues(top = topWindowInset))
)
}
}

View File

@@ -0,0 +1,26 @@
import extension.setupDependencyInjection
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
setupDependencyInjection()
android {
namespace = "io.element.android.features.networkmonitor.impl"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.features.networkmonitor.api)
}

View File

@@ -0,0 +1,10 @@
<!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View File

@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(FlowPreview::class)
package io.element.android.features.networkmonitor.impl
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
@ContributesBinding(scope = AppScope::class)
@SingleIn(AppScope::class)
class DefaultNetworkMonitor(
@ApplicationContext context: Context,
@AppCoroutineScope
appCoroutineScope: CoroutineScope,
) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
/**
* Calling connectivityManager methods synchronously from the callbacks is not safe.
* So instead we just keep the count of active networks, ie. those checking the capability request.
* Debounce the result to avoid quick offline<->online changes.
*/
val callback = object : ConnectivityManager.NetworkCallback() {
private val activeNetworksCount = AtomicInteger(0)
override fun onLost(network: Network) {
if (activeNetworksCount.decrementAndGet() == 0) {
trySendBlocking(NetworkStatus.Disconnected)
}
}
override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Connected)
}
}
}
trySendBlocking(connectivityManager.activeNetworkStatus())
val request = NetworkRequest.Builder().build()
connectivityManager.registerNetworkCallback(request, callback)
Timber.d("Subscribe")
awaitClose {
Timber.d("Unsubscribe")
connectivityManager.unregisterNetworkCallback(callback)
}
}
.distinctUntilChanged()
.debounce(300)
.onEach {
Timber.d("NetworkStatus changed=$it")
}
.stateIn(appCoroutineScope, SharingStarted.WhileSubscribed(), connectivityManager.activeNetworkStatus())
private fun ConnectivityManager.activeNetworkStatus(): NetworkStatus {
return if (activeNetwork != null) NetworkStatus.Connected else NetworkStatus.Disconnected
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.networkmonitor.test"
}
dependencies {
api(projects.features.networkmonitor.api)
api(libs.coroutines.core)
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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.MutableStateFlow
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Connected) : NetworkMonitor {
override val connectivity = MutableStateFlow(initialStatus)
}