There are a lot of applications in the play store that notifies you when your mobile goes offline. This article will help you understand how all that happens. How app knows that device is offline and notifies the user about the same by showing a notification on the screen. To understand this clearly I have created this small project on github.
How do we show the notification to the user when device goes offline
Your android device has capability to notify all the applications in the mobile when network status changes. The only thing your application needs to do is listen to that event. For that we need to create a broadcast receiver which can listen to this particular network status change event and take some action based on the network status.
For detailed explanation with demo, watch this
NetworkStateChangeReceiver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.ajit.singh.offlinemode.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import static android.content.Context.CONNECTIVITY_SERVICE;
public class NetworkStateChangeReceiver extends BroadcastReceiver {
public static final String NETWORK_AVAILABLE_ACTION = "com.ajit.singh.NetworkAvailable";
public static final String IS_NETWORK_AVAILABLE = "isNetworkAvailable";
@Override
public void onReceive(Context context, Intent intent) {
Intent networkStateIntent = new Intent(NETWORK_AVAILABLE_ACTION);
networkStateIntent.putExtra(IS_NETWORK_AVAILABLE, isConnectedToInternet(context));
LocalBroadcastManager.getInstance(context).sendBroadcast(networkStateIntent);
}
private boolean isConnectedToInternet(Context context) {
try {
if (context != null) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
return false;
} catch (Exception e) {
Log.e(NetworkStateChangeReceiver.class.getName(), e.getMessage());
return false;
}
}
}
NetworkStateChangeReceiver is a broadcast receiver which can listen to network status change event. When android triggers that event then NetworkStateChangeReceiver’s onReceive() method gets called. In onReceive() method you can write the logic to identify the network status and take some action.
isConnectedToInternet() method takes care of identifying wether the device is connected to internet or not. It does that using ConnectivityManager which has the information about the active network. You can use that NetworkInfo object to check the network status as shown in the code.
Adding receiver to AndroidManifest
Now that our NetworkStateChangeReceiver is ready and it can identify the network status but where is the code which tells that this receiver will listen to the network connectivity event. The answer is in the AndroidManifest.xml file. Your AndroidManifest will have that code. Let’s take a look at the AndroidManifest file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
<div class="post-mid-ad">
<div class="post-mid-ad__slot">
<ins class="adsbygoogle"
style="display:block; text-align:center;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-2886086145980317"
data-ad-slot="4015567555"></ins>
</div>
<div class="post-mid-ad__substack" aria-hidden="true">
<div class="inline-sub">
<div class="inline-sub__main">
<span class="inline-sub__badge" aria-hidden="true"><i class="fas fa-paper-plane"></i></span>
<div class="inline-sub__copy">
<span class="inline-sub__eyebrow">The Newsletter</span>
<h3 class="inline-sub__title">Never miss a post</h3>
<p class="inline-sub__pitch">New posts and the Dev Weekly roundup, straight to your inbox. Always free, no spam.</p>
</div>
</div>
<div class="inline-sub__form">
<div class="substack-embed" data-substack-embed>
<button type="button" class="substack-embed__load">
<span class="substack-embed__load-icon" aria-hidden="true"><i class="fas fa-envelope"></i></span>
<span class="substack-embed__load-text">Subscribe with email</span>
</button>
<iframe data-src="https://singhajitdotcom.substack.com/embed"
title="Subscribe to Ajit Singh on Substack"
loading="lazy"
scrolling="no"
hidden></iframe>
<script>
(function() {
var root = document.currentScript.closest('[data-substack-embed]');
if (!root || root.__substackBound) return;
root.__substackBound = true;
var OBSERVER_MARGIN = '200px 0px';
var btn = root.querySelector('.substack-embed__load');
var iframe = root.querySelector('iframe');
var label = "In-article Fallback";
var io = null;
var mo = null;
var engaged = false;
var observing = false;
var scrollOpts = { passive: true };
var clickOpts = { capture: true };
if (!btn || !iframe) return;
function cleanup() {
if (io) {
io.disconnect();
io = null;
}
if (mo) {
mo.disconnect();
mo = null;
}
window.removeEventListener('scroll', onEngage, scrollOpts);
document.removeEventListener('click', onEngage, clickOpts);
}
// In-article ad fallback stays in the viewport while hidden; don't fetch yet.
function isEligible() {
if (root.closest('[aria-hidden="true"]')) return false;
var shell = root.closest('.post-mid-ad__substack');
if (!shell) return true;
var wrap = shell.parentElement;
return !!(wrap && wrap.classList.contains('post-mid-ad--substack'));
}
function activate(fromClick) {
if (iframe.src) return;
if (!fromClick && !isEligible()) return;
var src = iframe.getAttribute('data-src');
if (!src) return;
cleanup();
iframe.src = src;
iframe.removeAttribute('data-src');
iframe.hidden = false;
btn.hidden = true;
root.classList.add('substack-embed--active');
if (fromClick && typeof gtag === 'function') {
gtag('event', 'subscribe_interaction', {
'event_category': 'Substack Subscribe',
'event_label': label
});
}
}
function tryAutoLoad() {
if (!isEligible()) return;
// After reveal, trust IntersectionObserver — avoid getBoundingClientRect
// right after classList / aria mutations (forced reflow).
activate(false);
}
function startObserver() {
if (observing || iframe.src) return;
observing = true;
if ('IntersectionObserver' in window) {
io = new IntersectionObserver(function(entries) {
for (var i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
tryAutoLoad();
return;
}
}
}, { rootMargin: OBSERVER_MARGIN });
io.observe(root);
var shell = root.closest('.post-mid-ad__substack');
if (shell && shell.parentElement) {
mo = new MutationObserver(function() {
if (!isEligible()) return;
// Let style settle, then rely on IO or activate if already near view.
requestAnimationFrame(function() {
tryAutoLoad();
});
});
mo.observe(shell.parentElement, { attributes: true, attributeFilter: ['class'] });
mo.observe(shell, { attributes: true, attributeFilter: ['aria-hidden'] });
}
} else {
tryAutoLoad();
}
}
function onEngage(e) {
if (engaged) return;
// Facade button owns its click (analytics + immediate load).
if (e && e.type === 'click' && btn.contains(e.target)) return;
engaged = true;
window.removeEventListener('scroll', onEngage, scrollOpts);
document.removeEventListener('click', onEngage, clickOpts);
startObserver();
}
btn.addEventListener('click', function() {
activate(true);
});
window.addEventListener('scroll', onEngage, scrollOpts);
document.addEventListener('click', onEngage, clickOpts);
})();
</script>
</div>
</div>
</div>
</div>
<script>
(function () {
var wrap = document.currentScript.parentNode;
var slot = wrap.querySelector('.post-mid-ad__slot');
var ins = wrap.querySelector('ins.adsbygoogle');
var fallback = wrap.querySelector('.post-mid-ad__substack');
if (!ins) return;
var settled = false;
function removeFallback() {
if (fallback && fallback.parentNode) fallback.remove();
fallback = null;
}
// Detect ad blockers via a static bait (#ad-blocker-bait) and the slot.
// Avoid appendChild + offsetHeight (forced reflow).
function adBlocked() {
try {
var s = window.getComputedStyle(ins);
if (s.display === 'none' || s.visibility === 'hidden') return true;
} catch (e) {}
try {
var bait = document.getElementById('ad-blocker-bait');
if (!bait) return false;
var cs = window.getComputedStyle(bait);
return cs.display === 'none' || cs.visibility === 'hidden';
} catch (e) {
return false;
}
}
// Ad served: keep it and drop the unused subscribe banner.
function keepAd() {
if (settled) return;
settled = true;
removeFallback();
}
// No ad: drop the ad slot and reveal the subscribe banner. The reserved
// height comes from CSS (.post-mid-ad min-height), and AdSense's unfilled
// collapse only hits the inner .post-mid-ad__slot, so the outer box keeps
// its size — the banner overlay fills it with no layout shift.
function showFallback() {
if (settled) return;
settled = true;
if (slot) slot.remove();
if (!fallback) return;
// Keep Substack behind click-to-load (no third-party cookies until intent).
wrap.classList.add('post-mid-ad--substack');
fallback.removeAttribute('aria-hidden');
}
// A real creative sets data-ad-status=filled. Unfilled ads may inject a
// 0-height iframe — do not use offsetHeight (forced reflow).
function adRendered() {
return ins.getAttribute('data-ad-status') === 'filled';
}
// Read data-ad-status. Note: unfilled ads still inject a 0-height iframe,
// so status must be checked before any iframe-presence heuristic.
function resolve(obs) {
var status = ins.getAttribute('data-ad-status');
if (status === 'unfilled') { obs.disconnect(); showFallback(); return true; }
if (status === 'filled') { obs.disconnect(); keepAd(); return true; }
return false;
}
function requestAd() {
if (settled) return;
// Defer geometry checks to the next frame so we don't force a sync
// reflow right after earlier DOM/style work.
requestAnimationFrame(function () {
if (settled) return;
// Slot never laid out (hidden container): nothing to show, nothing to shift.
// clientWidth avoids an extra layout path vs offsetWidth in some engines.
if (!(wrap.clientWidth > 0)) {
settled = true;
removeFallback();
wrap.style.display = 'none';
return;
}
if (adBlocked()) { showFallback(); return; }
try { (adsbygoogle = window.adsbygoogle || []).push({}); }
catch (e) { showFallback(); return; }
// Some blockers hide the slot right after the push.
if (adBlocked()) { showFallback(); return; }
var obs = new MutationObserver(function () { resolve(obs); });
obs.observe(ins, { attributes: true, attributeFilter: ['data-ad-status'] });
// Safety net for stuck slots, or when the AdSense script never loads
// (localhost) and no status is ever set.
setTimeout(function () {
if (settled) return;
if (resolve(obs)) return;
obs.disconnect();
if (adRendered()) keepAd(); else showFallback();
}, 3500);
});
}
// Request the ad only as the slot nears the viewport (no sync layout at parse).
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { io.disconnect(); requestAd(); }
}, { rootMargin: '1500px 0px' });
io.observe(wrap);
} else {
requestAd();
}
})();
</script>
</div>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ajit.singh.offlinemode">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver
android:name="com.ajit.singh.offlinemode.receiver.NetworkStateChangeReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
</application>
</manifest>
There are two things to notice here.
-
we have registered the receiver and added an intent filter which means that whenever an intent is fired by android which has android.net.conn.
CONNECTIVITY_CHANGEaction, our receiver will handle that intent. -
I have given three user permissions and all of them are to gain the network status access. Its very important to give these permissions otherwise you code will not work.
Show Notification
Now, we have our broadcast receiver in place and it can tell about the current internet status. But to show a notification we need UI and our ```NetworkStatusChangeReceiver`` doesn’t have a UI. To show the notification we need to pass a message to the current activity that show a notification with the network status.
To do that there should be a communication between Broadcast Receiver and the activity and to handle that we can broadcast a local message to the whole app and whichever activity is listening to that message will show the notification. In our case MainActivity is listening to this message and will show the notification using a Snackbar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.ajit.singh.offlinemode;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import com.ajit.singh.offlinemode.receiver.NetworkStateChangeReceiver;
import static com.ajit.singh.offlinemode.receiver.NetworkStateChangeReceiver.IS_NETWORK_AVAILABLE;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IntentFilter intentFilter = new IntentFilter(NetworkStateChangeReceiver.NETWORK_AVAILABLE_ACTION);
LocalBroadcastManager.getInstance(this).registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean isNetworkAvailable = intent.getBooleanExtra(IS_NETWORK_AVAILABLE, false);
String networkStatus = isNetworkAvailable ? "connected" : "disconnected";
Snackbar.make(findViewById(R.id.activity_main), "Network Status: " + networkStatus, Snackbar.LENGTH_LONG).show();
}
}, intentFilter);
}
}
Our main activity has registered to a broadcast which has com.ajit.singh.NetworkAvailable action because thats the action we are using to broadcast the intent from the NetworkStatusChangeReceiver. In its onReceive() method we are getting the status of the network from the intent which we had set in the NetworkStatusChangeReceiver. Then final thing is show that status using a Snackbar.
Thats all folks, I hope you liked this post. Thanks!