41

Problem:

So the problem is that I have an app which sends a request to our backend whenever WiFi is connected (with the connected SSID and other info) or when it is disconnected (over the mobile network). However with the changes in Android 7/N and above, CONNECTIVITY_CHANGE and CONNECTIVITY_ACTION no longer work in the background. Now in most cases people misuse this broadcast and as such I can completely understand why the change was made. However, I have no idea how to solve this problem in the current state.

Now I'm not at all much of an Android developer (this is for a Cordova plugin) so I'm counting on you guys!

Expected behavior: App is woken up and request is sent whenever WiFi switches connectivity, even when app is killed/in background.

Current behavior: App only sends request when the app is in the foreground.

Tried so far: So far I've moved the implicit intent to listen to CONNECTIVITY_ACTION from the manifest to manually registering it in the main part of the app (plugin). This makes it work as long as the app is in memory but not on cold boot or actual background

Already looked at: Most answers talk about using scheduled jobs to substitute for the missing broadcast. I see how this works for, for example, retrying a download or similar, but not for my case (but please correct me if I'm wrong). Below are the SO posts I've already looked at:

Detect connectivity changes on Android 7.0 Nougat when app is in foreground

ConnectivityManager.CONNECTIVITY_ACTION deprecated

Detect Connectivity change using JobScheduler

Android O - Detect connectivity change in background

Remy Kabel
  • 946
  • 1
  • 7
  • 15

5 Answers5

66

Nougat and Above: We have to use JobScheduler and JobService for Connection Changes.

All I can divide this into three steps.

Register JobScheduler inside activity. Also, Start JobService( Service to handle callbacks from the JobScheduler. Requests scheduled with the JobScheduler ultimately land on this service's "onStartJob" method.)

public class NetworkConnectionActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_network_connection);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        scheduleJob();

    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void scheduleJob() {
        JobInfo myJob = new JobInfo.Builder(0, new ComponentName(this, NetworkSchedulerService.class))
                .setRequiresCharging(true)
                .setMinimumLatency(1000)
                .setOverrideDeadline(2000)
                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
                .setPersisted(true)
                .build();

        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        jobScheduler.schedule(myJob);
    }

    @Override
    protected void onStop() {
        // A service can be "started" and/or "bound". In this case, it's "started" by this Activity
        // and "bound" to the JobScheduler (also called "Scheduled" by the JobScheduler). This call
        // to stopService() won't prevent scheduled jobs to be processed. However, failing
        // to call stopService() would keep it alive indefinitely.
        stopService(new Intent(this, NetworkSchedulerService.class));
        super.onStop();
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Start service and provide it a way to communicate with this class.
        Intent startServiceIntent = new Intent(this, NetworkSchedulerService.class);
        startService(startServiceIntent);
    }
}

The service to start and finish the job.

public class NetworkSchedulerService extends JobService implements
        ConnectivityReceiver.ConnectivityReceiverListener {

    private static final String TAG = NetworkSchedulerService.class.getSimpleName();

    private ConnectivityReceiver mConnectivityReceiver;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "Service created");
        mConnectivityReceiver = new ConnectivityReceiver(this);
    }



    /**
     * When the app's NetworkConnectionActivity is created, it starts this service. This is so that the
     * activity and this service can communicate back and forth. See "setUiCallback()"
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand");
        return START_NOT_STICKY;
    }


    @Override
    public boolean onStartJob(JobParameters params) {
        Log.i(TAG, "onStartJob" + mConnectivityReceiver);
        registerReceiver(mConnectivityReceiver, new IntentFilter(Constants.CONNECTIVITY_ACTION));
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.i(TAG, "onStopJob");
        unregisterReceiver(mConnectivityReceiver);
        return true;
    }

    @Override
    public void onNetworkConnectionChanged(boolean isConnected) {
        String message = isConnected ? "Good! Connected to Internet" : "Sorry! Not connected to internet";
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();

    }
}

Finally, The receiver class which checks the network connection changes.

public class ConnectivityReceiver extends BroadcastReceiver {

    private ConnectivityReceiverListener mConnectivityReceiverListener;

    ConnectivityReceiver(ConnectivityReceiverListener listener) {
        mConnectivityReceiverListener = listener;
    }


    @Override
    public void onReceive(Context context, Intent intent) {
        mConnectivityReceiverListener.onNetworkConnectionChanged(isConnected(context));

    }

    public static boolean isConnected(Context context) {
        ConnectivityManager cm = (ConnectivityManager)
                context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
    }

    public interface ConnectivityReceiverListener {
        void onNetworkConnectionChanged(boolean isConnected);
    }
}

Don't forget to add permission and service inside manifest file.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.yourpackagename">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>


    <!-- Always required on api < 21, needed to keep a wake lock while your job is running -->
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <!-- Required on api < 21 if you are using setRequiredNetworkType(int) -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <!-- Required on all api levels if you are using setPersisted(true) -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".connectivity.NetworkConnectionActivity"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>


        <!-- Define your service, make sure to add the permision! -->
        <service
            android:name=".connectivity.NetworkSchedulerService"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE"/>
    </application>

</manifest>

Please refer below links for more info.

https://github.com/jiteshmohite/Android-Network-Connectivity

https://github.com/evant/JobSchedulerCompat

https://github.com/googlesamples/android-JobScheduler

https://medium.com/@iiro.krankka/its-time-to-kiss-goodbye-to-your-implicit-broadcastreceivers-eefafd9f4f8a

Jitesh Mohite
  • 31,138
  • 12
  • 157
  • 147
  • Thank you for this elaborate answer! (others as well) My coworker wil be looking into this at a later moment. However, the information looks great – Remy Kabel Feb 21 '18 at 16:51
  • Does this actually work for you? I tried it out and the service gets destroyed almost immediately and is not restarted. – Philipp E. Mar 03 '18 at 17:18
  • [jithesh] Hey! I tried using your code. It works fine when the app is open. But when I kill the app, I don't receive any notification when there is network change. As per doc, JobScheduler is supposed to work even when app is killed. Can you please help me with this? Thanks – shakunthalaMK Mar 28 '18 at 09:26
  • @sha: Sure, Will be happy to help you. What do you mean by "don't receive any notification"? – Jitesh Mohite Mar 28 '18 at 10:21
  • Thanks a lot. I meant the service is stopped when I kill the app. Hence, when I turn off/on the data I don't get any Toast message related to that like how we get when the app is open. – shakunthalaMK Mar 28 '18 at 10:47
  • @sha: Have you run this sample on your device? https://github.com/jiteshmohite/Android-Network-Connectivity. I created this repository for the same problem. – Jitesh Mohite Mar 28 '18 at 10:53
  • Yes, I am using the same. does it work for you when you kill the app also? – shakunthalaMK Mar 28 '18 at 11:11
  • @sha: It works for me. Check your device have all above mention permission. – Jitesh Mohite Mar 28 '18 at 11:14
  • Ok. Between, I am working on Oreo device. On which device did you test? – shakunthalaMK Mar 28 '18 at 11:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/167722/discussion-between-jitesh-mohite-and-sha). – Jitesh Mohite Mar 28 '18 at 11:20
  • It's not working on Android Oreo. When I swipe-to-kill the app, no more state changes are received – mbo Sep 20 '18 at 07:41
  • @Marius: It is working on an oreo device. I checked on the emulator where onStartJob gets called even if you killed the application. I running this on android version 8.1.0. What is your Android device version, if possible please mention device? – Jitesh Mohite Sep 27 '18 at 02:37
  • I haven't tried it. But looking at the problem mentioned about Oreo device, there is one possibility. Jobscheduler won't necessarily trigger immediately after the connectivity change. It is optimizing for battery and it tries to bunch requests together and triggers them when it thinks is right moment. Emulator is typically considered in charging state. JobScheduler may be triggering jobs immediate in charging state may be. That may explain why people get different results between real device and emulator. – rpattabi Sep 27 '18 at 15:10
  • 3
    By the way, CONNECTIVITY_ACTION is deprecated in API 28 (Pie) https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION – rpattabi Sep 27 '18 at 15:12
  • 1
    Be careful forgetting "setRequiresCharging(true)" in, will make it that you require the phone to be charging for the Job to fire. – Abushawish Oct 02 '18 at 21:07
  • @Abushawish: Do you mean the phone should connect with charger to fire this job? – Jitesh Mohite Oct 03 '18 at 05:48
  • @jiteshmohite Yes, so set that boolean to "false" if you want that job to fire regardless or being connected to charger or not. – Abushawish Oct 03 '18 at 15:57
  • @Abushawish: I guess by default this value is false. – Jitesh Mohite Oct 04 '18 at 05:47
  • It is working with android versions below LOLLIPOP ? – Alberto Crespo Nov 14 '18 at 15:49
  • 2
    Does the broadcast receiver continue to work when the app is swiped out from recent apps? (I tried it on Goole Pixel with Oreo and it didn't work) – Keselme Dec 19 '18 at 08:28
  • 3
    I only want to know if network connectivity change, I don't know why Android makes it terrible – Văn Quyết Jan 17 '19 at 11:04
6

The best way to grab Connectivity change Android Os 7 and above is register your ConnectivityReceiver broadcast in Application class like below, This helps you to get changes in background as well until your app alive.

public class MyApplication extends Application {

      private ConnectivityReceiver connectivityReceiver;

      private ConnectivityReceiver getConnectivityReceiver() {
          if (connectivityReceiver == null)
               connectivityReceiver = new ConnectivityReceiver();

          return connectivityReceiver;
       }
       @Override
       public void onCreate() {
         super.onCreate();
         registerConnectivityReceiver();
       }

     // register here your filtters 
     private void registerConnectivityReceiver(){
       try {
          // if (android.os.Build.VERSION.SDK_INT >= 26) {
          IntentFilter filter = new IntentFilter();
          filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
          //filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
          //filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
          //filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
          registerReceiver(getConnectivityReceiver(), filter);
       } catch (Exception e) {
         MLog.e(TAG, e.getMessage());
       }
 }

}

And then in manifest

     <application
      android:name=".app.MyApplication"/>

Here is your ConnectivityReceiver.java

 public class ConnectivityReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(final Context context, final Intent intent) {
      MLog.v(TAG, "onReceive().." + intent.getAction());
      }
    }
Guruprasad
  • 931
  • 11
  • 16
3

That's how i did it. I have created a IntentService and in onCreate method and I have registered networkBroadacst which check for internet connection.

public class SyncingIntentService extends IntentService {
    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            networkBroadcast=new NetworkBroadcast();
            registerReceiver(networkBroadcast,
                  new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
        }
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        onHandleIntent(intent);
        return START_STICKY;
    }
}

This is my broadcast class

public class NetworkBroadcast extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Constants.isInternetConnected(context)) {
//            Toast.makeText(context, "Internet Connect", Toast.LENGTH_SHORT).show();
           context.startService(new Intent(context, SyncingIntentService.class));
        }
        else{}
    }
}

In this way you can check internet connection in whether your app is in foreground or background in nougat.

A. Badakhshan
  • 1,045
  • 7
  • 22
Mohit Hooda
  • 273
  • 3
  • 9
  • 1
    What happens if you application dies? Will the service stay? There's a bug that causes your service to die as soon as your activity is stopped regardless of whether the service and application are in isolated processes. – TheRealChx101 Sep 13 '18 at 20:57
  • i dont think so. the issue is that connectivity_change doesnt work anymore in N above so the receiver is useless since it cannot get those events. – chitgoks Nov 10 '19 at 14:48
3

Below is excerpt from documentation

Apps targeting Android 7.0 (API level 24) and higher do not receive CONNECTIVITY_ACTION broadcasts if they declare the broadcast receiver in their manifest. Apps will still receive CONNECTIVITY_ACTION broadcasts if they register their BroadcastReceiver with Context.registerReceiver() and that context is still valid.

So you will get this Broadcast till your context is valid in Android N & above by explicitly registering for same.

Boot Completed:

You can listen android.intent.action.BOOT_COMPLETED broadcast you will need this permission for same.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

App Killed Scenario:

You are not going to receive it.

That is very much expected and due to various reasons

Akhil
  • 6,667
  • 4
  • 31
  • 61
  • They cannot be serious!!! But I fear you are right... What if there are apps like time-tracking which want to automate start/stop via connected WiFi network? – mbo Sep 20 '18 at 07:48
  • @Marius That's why you use JobScheduler. For newer versions and still keep `CONNECTIVITY_CHANGE` in manifest for older Android versions – Pierre Jul 02 '19 at 05:30
  • Google is extremely serious about their bad APIs, and about the random breaking changes that they keep making to make their bad APIs worse. – Andrew Koster Sep 29 '19 at 23:01
0

Another approach which is simpler and easier when you use registerNetworkCallback (NetworkRequest, PendingIntent):

NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
builder.addTransportType(NetworkCapabilities.TRANSPORT_VPN);

ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
Intent intent = new Intent(this, SendAnyRequestService.class);

PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (connectivityManager != null) {
    NetworkRequest networkRequest = builder.build();
    connectivityManager.registerNetworkCallback(networkRequest, pendingIntent);
}

Which is SendAnyRequestService.class is your service class, and you can call your API inside it.

This code work for Android 6.0 (API 23) and above

Ref document is here

Frank Nguyen
  • 6,493
  • 3
  • 38
  • 37