diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
index f29aa39607..83105c678a 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
@@ -16,8 +16,7 @@ import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.views.call.CallActivity
-import chat.simplex.common.helpers.APPLICATION_ID
-import chat.simplex.common.helpers.requiresIgnoringBattery
+import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.updatingChatsMutex
@@ -291,6 +290,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
activeCallDestroyWebView()
}
+ override fun androidRestartNetworkObserver() {
+ NetworkObserver.shared.restartNetworkObserver()
+ }
+
@SuppressLint("SourceLockedOrientationActivity")
@Composable
override fun androidLockPortraitOrientation() {
diff --git a/apps/multiplatform/common/src/androidMain/AndroidManifest.xml b/apps/multiplatform/common/src/androidMain/AndroidManifest.xml
index 74520465ae..eeb822503d 100644
--- a/apps/multiplatform/common/src/androidMain/AndroidManifest.xml
+++ b/apps/multiplatform/common/src/androidMain/AndroidManifest.xml
@@ -1,4 +1,5 @@
+
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt
new file mode 100644
index 0000000000..825bc8b846
--- /dev/null
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt
@@ -0,0 +1,104 @@
+package chat.simplex.common.helpers
+
+import android.net.*
+import android.util.Log
+import androidx.core.content.getSystemService
+import chat.simplex.common.model.ChatModel.controller
+import chat.simplex.common.model.UserNetworkInfo
+import chat.simplex.common.model.UserNetworkType
+import chat.simplex.common.platform.*
+import chat.simplex.common.views.helpers.withBGApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+
+class NetworkObserver {
+ private var prevInfo: UserNetworkInfo? = null
+
+ // When having both mobile and Wi-Fi networks enabled with Wi-Fi being active, then disabling Wi-Fi, network reports its offline (which is true)
+ // but since it will be online after switching to mobile, there is no need to inform backend about such temporary change.
+ // But if it will not be online after some seconds, report it and apply required measures
+ private var noNetworkJob = Job() as Job
+ private val networkCallback = object: ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) = networkCapabilitiesChanged(networkCapabilities)
+ override fun onLost(network: Network) = networkLost()
+ }
+ private val connectivityManager: ConnectivityManager? = androidAppContext.getSystemService()
+
+ fun restartNetworkObserver() {
+ if (connectivityManager == null) {
+ Log.e(TAG, "Connectivity manager is unavailable, network observer is disabled")
+ val info = UserNetworkInfo(
+ networkType = UserNetworkType.OTHER,
+ online = true,
+ )
+ prevInfo = info
+ setNetworkInfo(info)
+ return
+ }
+ try {
+ connectivityManager.unregisterNetworkCallback(networkCallback)
+ } catch (e: Exception) {
+ // do nothing
+ }
+ val initialCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ if (initialCapabilities != null) {
+ networkCapabilitiesChanged(initialCapabilities)
+ } else {
+ networkLost()
+ }
+ try {
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error registering network callback: ${e.stackTraceToString()}")
+ }
+ }
+
+ private fun networkCapabilitiesChanged(capabilities: NetworkCapabilities) {
+ connectivityManager ?: return
+ val info = UserNetworkInfo(
+ networkType = networkTypeFromCapabilities(capabilities),
+ online = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
+ )
+ if (prevInfo != info) {
+ prevInfo = info
+ setNetworkInfo(info)
+ }
+ }
+
+ private fun networkLost() {
+ Log.d(TAG, "Network changed: lost")
+ val none = UserNetworkInfo(networkType = UserNetworkType.NONE, false)
+ prevInfo = none
+ setNetworkInfo(none)
+ }
+
+ private fun setNetworkInfo(info: UserNetworkInfo) {
+ Log.d(TAG, "Network changed: $info")
+ noNetworkJob.cancel()
+ if (info.online) {
+ withBGApi {
+ if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) {
+ chatModel.networkInfo.value = info
+ }
+ }
+ } else {
+ noNetworkJob = withBGApi {
+ delay(3000)
+ if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) {
+ chatModel.networkInfo.value = info
+ }
+ }
+ }
+ }
+
+ private fun networkTypeFromCapabilities(capabilities: NetworkCapabilities): UserNetworkType = when {
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> UserNetworkType.ETHERNET
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> UserNetworkType.WIFI
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> UserNetworkType.CELLULAR
+ else -> UserNetworkType.OTHER
+ }
+
+ companion object {
+ val shared = NetworkObserver()
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
index a8a5797d71..0e15079ac5 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
@@ -118,6 +118,7 @@ object ChatModel {
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
val clipboardHasText = mutableStateOf(false)
+ val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true))
val updatingChatsMutex: Mutex = Mutex()
val changingActiveUserMutex: Mutex = Mutex()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 7848f9942e..484990bfc0 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -358,6 +358,8 @@ object ChatController {
var lastMsgReceivedTimestamp: Long = System.currentTimeMillis()
private set
+ fun hasChatCtrl() = ctrl != -1L && ctrl != null
+
private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock {
val userId = chatModel.currentUser.value?.userId
if (userId == null) {
@@ -879,6 +881,9 @@ object ChatController {
}
}
+ suspend fun apiSetNetworkInfo(networkInfo: UserNetworkInfo): Boolean =
+ sendCommandOkResp(null, CC.APISetNetworkInfo(networkInfo))
+
suspend fun apiSetMemberSettings(rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings): Boolean =
sendCommandOkResp(rh, CC.ApiSetMemberSettings(groupId, groupMemberId, memberSettings))
@@ -2413,6 +2418,7 @@ sealed class CC {
class APIGetChatItemTTL(val userId: Long): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
class APIGetNetworkConfig: CC()
+ class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC()
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC()
class APIContactInfo(val contactId: Long): CC()
@@ -2555,6 +2561,7 @@ sealed class CC {
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
is APIGetNetworkConfig -> "/network"
+ is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
is APIContactInfo -> "/_info @$contactId"
@@ -2692,6 +2699,7 @@ sealed class CC {
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetNetworkConfig -> "apiSetNetworkConfig"
is APIGetNetworkConfig -> "apiGetNetworkConfig"
+ is APISetNetworkInfo -> "apiSetNetworkInfo"
is APISetChatSettings -> "apiSetChatSettings"
is ApiSetMemberSettings -> "apiSetMemberSettings"
is APIContactInfo -> "apiContactInfo"
@@ -5531,3 +5539,26 @@ enum class AppSettingsLockScreenCalls {
}
}
}
+
+@Serializable
+data class UserNetworkInfo(
+ val networkType: UserNetworkType,
+ val online: Boolean,
+)
+
+enum class UserNetworkType {
+ @SerialName("none") NONE,
+ @SerialName("cellular") CELLULAR,
+ @SerialName("wifi") WIFI,
+ @SerialName("ethernet") ETHERNET,
+ @SerialName("other") OTHER;
+
+ val text: String
+ get() = when (this) {
+ NONE -> generalGetString(MR.strings.network_type_no_network_connection)
+ CELLULAR -> generalGetString(MR.strings.network_type_cellular)
+ WIFI -> generalGetString(MR.strings.network_type_network_wifi)
+ ETHERNET -> generalGetString(MR.strings.network_type_ethernet)
+ OTHER -> generalGetString(MR.strings.network_type_other)
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
index f1a6d35e45..00370f5231 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt
@@ -9,7 +9,6 @@ import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import kotlinx.coroutines.*
-import kotlinx.serialization.decodeFromString
import java.io.File
import java.nio.ByteBuffer
@@ -88,6 +87,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
Log.d(TAG, "Unable to migrate successfully: $res")
return
}
+ platform.androidRestartNetworkObserver()
controller.apiSetTempFolder(coreTmpDir.absolutePath)
controller.apiSetFilesFolder(appFilesDir.absolutePath)
if (appPlatform.isDesktop) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
index 5ca6dbdeb3..f61c5bc83e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
@@ -23,6 +23,7 @@ interface PlatformInterface {
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
+ fun androidRestartNetworkObserver() {}
@Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopScrollBarComponents(): Triple, Modifier, MutableState> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) }
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
index ae09163591..182861c0d6 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt
@@ -15,7 +15,6 @@ import chat.simplex.res.MR
import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.io.*
import java.net.URI
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt
index e1f050b0b6..4d33040e29 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
+import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionView
@@ -21,12 +22,12 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.input.*
import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
-import chat.simplex.common.platform.ColumnWithScrollBar
-import chat.simplex.common.platform.chatModel
+import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ClickableText
import chat.simplex.common.views.helpers.*
@@ -190,6 +191,16 @@ fun NetworkAndServersView() {
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
}
+
+ if (appPlatform.isAndroid) {
+ SectionDividerSpaced()
+ SectionView(generalGetString(MR.strings.settings_section_title_network_connection).uppercase()) {
+ val info = remember { chatModel.networkInfo }.value
+ SettingsActionItemWithContent(icon = null, info.networkType.text) {
+ Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = if (info.online) Color.Green else MaterialTheme.colors.error)
+ }
+ }
+ }
SectionBottomSpacer()
}
}
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index 5595f319ea..eafe959640 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -1017,6 +1017,7 @@
THEMES
MESSAGES AND FILES
CALLS
+ Network connection
Incognito mode
EXPERIMENTAL
Use from desktop
@@ -1925,4 +1926,11 @@
Check your internet connection and try again
Warning: the archive will be deleted.]]>
Error verifying passphrase:
+
+
+ No network connection
+ Cellular
+ WiFi
+ Wired ethernet
+ Other
\ No newline at end of file