Skip to content

Commit 34ea5db

Browse files
committed
Merge branch 'puneet/6029_ConnectivityManager' into shubham/full-oma-experience
2 parents f462d98 + 9b1ee10 commit 34ea5db

File tree

4 files changed

+147
-24
lines changed

4 files changed

+147
-24
lines changed

toolkit/offline/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
-->
1818
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1919

20+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2021
<application>
2122
<service
2223
android:name="androidx.work.impl.foreground.SystemForegroundService"

toolkit/offline/src/main/java/com/arcgismaps/toolkit/offline/OfflineMapAreas.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import androidx.compose.ui.Modifier
3434
import androidx.compose.ui.platform.LocalContext
3535
import com.arcgismaps.mapping.ArcGISMap
3636
import com.arcgismaps.toolkit.offline.internal.utils.AddMapAreaButton
37+
import com.arcgismaps.toolkit.offline.internal.utils.NetworkConnectionState
38+
import com.arcgismaps.toolkit.offline.internal.utils.networkConnectivityState
3739
import com.arcgismaps.toolkit.offline.internal.utils.getDefaultMapAreaTitle
3840
import com.arcgismaps.toolkit.offline.internal.utils.isValidMapAreaTitle
3941
import com.arcgismaps.toolkit.offline.ondemand.OnDemandMapAreaConfiguration
@@ -62,11 +64,19 @@ public fun OfflineMapAreas(
6264
val initializationStatus by offlineMapState.initializationStatus
6365
var isRefreshEnabled by rememberSaveable { mutableStateOf(false) }
6466

67+
val internetConnectionState by networkConnectivityState()
68+
LaunchedEffect(internetConnectionState) {
69+
isRefreshEnabled = true
70+
}
71+
6572
LaunchedEffect(offlineMapState, isRefreshEnabled) {
6673
if (isRefreshEnabled) {
6774
offlineMapState.resetInitialize()
6875
}
69-
offlineMapState.initialize(context)
76+
offlineMapState.initialize(
77+
context,
78+
isDeviceOffline = internetConnectionState == NetworkConnectionState.Unavailable
79+
)
7080
isRefreshEnabled = false
7181
}
7282

@@ -100,7 +110,7 @@ public fun OfflineMapAreas(
100110
PreplannedLayoutContainer(
101111
modifier = modifier,
102112
preplannedMapAreaStates = offlineMapState.preplannedMapAreaStates,
103-
isShowingOnlyOfflineModels = offlineMapState.isShowingOnlyOfflineModels,
113+
isShowingOnlyOfflineModels = internetConnectionState == NetworkConnectionState.Unavailable,
104114
onDownloadDeleted = offlineMapState::removePreplannedMapArea,
105115
onRefresh = { isRefreshEnabled = true }
106116
)
@@ -110,7 +120,7 @@ public fun OfflineMapAreas(
110120
OnDemandLayoutContainer(
111121
modifier = modifier,
112122
onDemandMapAreaStates = offlineMapState.onDemandMapAreaStates,
113-
isShowingOnlyOfflineModels = offlineMapState.isShowingOnlyOfflineModels,
123+
isShowingOnlyOfflineModels = internetConnectionState == NetworkConnectionState.Unavailable,
114124
localMap = offlineMapState.localMap,
115125
onRefresh = { isRefreshEnabled = true },
116126
onDownloadDeleted = offlineMapState::removeOnDemandMapArea,

toolkit/offline/src/main/java/com/arcgismaps/toolkit/offline/OfflineMapState.kt

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@ public class OfflineMapState(
9696
*/
9797
public val initializationStatus: State<InitializationStatus> = _initializationStatus
9898

99-
/**
100-
* A Boolean value indicating if only offline models are being shown.
101-
*
102-
* @since 200.8.0
103-
*/
104-
internal var isShowingOnlyOfflineModels by mutableStateOf(false)
105-
private set
106-
10799
/**
108100
* A Boolean value indicating whether the web map is offline disabled.
109101
*
@@ -118,22 +110,16 @@ public class OfflineMapState(
118110
* @return the [Result] indicating if the initialization was successful or not
119111
* @since 200.8.0
120112
*/
121-
internal suspend fun initialize(context: Context): Result<Unit> = runCatchingCancellable {
113+
internal suspend fun initialize(context: Context, isDeviceOffline: Boolean): Result<Unit> = runCatchingCancellable {
122114
if (_initializationStatus.value is InitializationStatus.Initialized) {
123115
return Result.success(Unit)
124116
}
125117
_initializationStatus.value = InitializationStatus.Initializing
126118
// initialize the offline repository
127119
OfflineRepository.refreshOfflineMapInfos(context)
128-
// reset to check if map has offline enabled
129-
isShowingOnlyOfflineModels = false
130120
// load the map, and ignore network error if device is offline
131121
arcGISMap.retryLoad().getOrElse { error ->
132-
// check if the error is due to network connection
133-
if (error.message?.contains("Unable to resolve host") == true) {
134-
// enable offline only mode
135-
isShowingOnlyOfflineModels = true
136-
} else {
122+
if (!isDeviceOffline) {
137123
// unexpected error, report failed status
138124
_initializationStatus.value = InitializationStatus.FailedToInitialize(error)
139125
throw error
@@ -146,8 +132,7 @@ public class OfflineMapState(
146132

147133
// load the task, and ignore network error if device is offline
148134
offlineMapTask.retryLoad().getOrElse { error ->
149-
// check if the error is not due to network connection
150-
if (error.message?.contains("Unable to resolve host") == false) {
135+
if (!isDeviceOffline) {
151136
// unexpected error, report failed status
152137
_initializationStatus.value = InitializationStatus.FailedToInitialize(error)
153138
throw error
@@ -159,7 +144,7 @@ public class OfflineMapState(
159144
(arcGISMap.loadStatus.value == LoadStatus.Loaded) && (arcGISMap.offlineSettings == null)
160145

161146
// load the preplanned map area states
162-
loadPreplannedMapAreas(context)
147+
loadPreplannedMapAreas(context, isDeviceOffline)
163148

164149
// check if preplanned for loaded
165150
if (_mode != OfflineMapMode.Preplanned || _mode == OfflineMapMode.Unknown) {
@@ -177,7 +162,7 @@ public class OfflineMapState(
177162
*
178163
* @since 200.8.0
179164
*/
180-
private suspend fun loadPreplannedMapAreas(context: Context) {
165+
private suspend fun loadPreplannedMapAreas(context: Context, isDeviceOffline: Boolean) {
181166
_preplannedMapAreaStates.clear()
182167
val preplannedMapAreas = mutableListOf<PreplannedMapArea>()
183168
try {
@@ -188,7 +173,7 @@ public class OfflineMapState(
188173
// an exception will be thrown in offline mode
189174
preplannedMapAreas.clear()
190175
}
191-
if (isShowingOnlyOfflineModels || preplannedMapAreas.isEmpty()) {
176+
if (isDeviceOffline || preplannedMapAreas.isEmpty()) {
192177
loadOfflinePreplannedMapAreas(context = context)
193178
} else {
194179
_mode = OfflineMapMode.Preplanned
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
*
3+
* Copyright 2025 Esri
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
package com.arcgismaps.toolkit.offline.internal.utils
19+
20+
import android.content.Context
21+
import android.net.ConnectivityManager
22+
import android.net.Network
23+
import android.net.NetworkCapabilities
24+
import android.net.NetworkRequest
25+
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.produceState
27+
import androidx.compose.ui.platform.LocalContext
28+
import kotlinx.coroutines.channels.awaitClose
29+
import kotlinx.coroutines.flow.callbackFlow
30+
import androidx.compose.runtime.State
31+
32+
@Composable
33+
internal fun networkConnectivityState(): State<NetworkConnectionState> {
34+
val context = LocalContext.current
35+
36+
// Creates a State<NetworkConnectionState> with current connectivity state as initial value
37+
return produceState(initialValue = context.currentConnectivityState) {
38+
// In a coroutine, can make suspend calls
39+
context.observeConnectivityAsFlow().collect { value = it }
40+
}
41+
}
42+
43+
internal fun Context.observeConnectivityAsFlow() = callbackFlow {
44+
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
45+
46+
val callback = NetworkCallback(connectivityManager) { networkConnectionState -> trySend(networkConnectionState) }
47+
48+
val networkRequest = NetworkRequest.Builder()
49+
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
50+
.build()
51+
52+
connectivityManager.registerNetworkCallback(networkRequest, callback)
53+
54+
// Set current state
55+
val currentState = getCurrentConnectivityState(connectivityManager)
56+
trySend(currentState)
57+
58+
// Remove callback when not used
59+
awaitClose {
60+
// Remove listeners
61+
connectivityManager.unregisterNetworkCallback(callback)
62+
}
63+
}
64+
65+
internal fun NetworkCallback(
66+
connectivityManager: ConnectivityManager,
67+
callback: (NetworkConnectionState) -> Unit
68+
): ConnectivityManager.NetworkCallback {
69+
return object : ConnectivityManager.NetworkCallback() {
70+
/**
71+
* This callback is triggered when a network is available.
72+
* It indicates that the device has internet connectivity.
73+
*/
74+
override fun onAvailable(network: Network) {
75+
callback(NetworkConnectionState.Available)
76+
}
77+
78+
/**
79+
* This callback is triggered when a network is temporarily unavailable.
80+
* It does not necessarily mean that there is no internet connectivity,
81+
* but rather that the network is in a transient state (e.g., switching networks).
82+
*/
83+
override fun onLost(network: Network) {
84+
// This callback is triggered when any previously available network is lost,
85+
// not just when all internet connectivity is lost. If the device switches
86+
// between networks (e.g., from Wi-Fi to mobile data), onLost is called for
87+
// the old network, even if another network is still available.
88+
val state = getCurrentConnectivityState(connectivityManager)
89+
callback(state)
90+
}
91+
}
92+
}
93+
94+
/**
95+
* Network utility to get current state of internet connection
96+
*/
97+
internal val Context.currentConnectivityState: NetworkConnectionState
98+
get() {
99+
val connectivityManager =
100+
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
101+
return getCurrentConnectivityState(connectivityManager)
102+
}
103+
104+
/**
105+
* Helper function to determine the current connectivity state.
106+
*/
107+
private fun getCurrentConnectivityState(
108+
connectivityManager: ConnectivityManager
109+
): NetworkConnectionState {
110+
val connected = connectivityManager.allNetworks.any { network ->
111+
connectivityManager.getNetworkCapabilities(network)
112+
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
113+
}
114+
115+
return if (connected) NetworkConnectionState.Available else NetworkConnectionState.Unavailable
116+
}
117+
118+
/**
119+
* Represents the state of network connectivity.
120+
*
121+
* - [Available]: Indicates that the device has internet connectivity.
122+
* - [Unavailable]: Indicates that the device does not have internet connectivity.
123+
*/
124+
internal sealed class NetworkConnectionState {
125+
object Available : NetworkConnectionState()
126+
object Unavailable : NetworkConnectionState()
127+
}

0 commit comments

Comments
 (0)