From b84ef0ce353dfd8df4416a74a50a59ab6789846d Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:26:24 +0700 Subject: [PATCH] android: network observer (#3992) * android: network observer * removed unused code * rename * rename * new line * rename * changes in logic * adapted to ios logic * change * change * change * networkLost * item in settings * reorder * changes --------- Co-authored-by: Evgeny Poberezkin --- .../main/java/chat/simplex/app/SimplexApp.kt | 7 +- .../src/androidMain/AndroidManifest.xml | 1 + .../simplex/common/helpers/NetworkObserver.kt | 104 ++++++++++++++++++ .../chat/simplex/common/model/ChatModel.kt | 1 + .../chat/simplex/common/model/SimpleXAPI.kt | 31 ++++++ .../chat/simplex/common/platform/Core.kt | 2 +- .../chat/simplex/common/platform/Platform.kt | 1 + .../simplex/common/views/helpers/Utils.kt | 1 - .../views/usersettings/NetworkAndServers.kt | 15 ++- .../commonMain/resources/MR/base/strings.xml | 8 ++ 10 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt 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