16

First off: I know that ConnectivityManager.CONNECTIVITY_ACTION has been deprecated and I know how to use connectivityManager.registerNetworkCallback. Furthermore if read about JobScheduler, but I am not entirely sure whether I got it right.

My problem is that I want to execute some code when the phone is connected / disconnected to/from a network. This should happen when the app is in the background as well. Starting with Android O I would have to show a notification if I want to run a service in the background what I want to avoid.
I tried getting info on when the phone connects / disconnects using the JobScheduler/JobService APIs, but it only gets executed the time I schedule it. For me it seems like I can't run code when an event like this happens. Is there any way to achieve this? Do I maybe just have to adjust my code a bit?

My JobService:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class ConnectivityBackgroundServiceAPI21 extends JobService {

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        LogFactory.writeMessage(this, LOG_TAG, "Job was started");
        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
        if (activeNetwork == null) {
            LogFactory.writeMessage(this, LOG_TAG, "No active network.");
        }else{
            // Here is some logic consuming whether the device is connected to a network (and to which type)
        }
        LogFactory.writeMessage(this, LOG_TAG, "Job is done. ");
        return false;
    }
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
    LogFactory.writeMessage(this, LOG_TAG, "Job was stopped");
    return true;
}

I start the service like so:

JobScheduler jobScheduler = (JobScheduler)context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName service = new ComponentName(context, ConnectivityBackgroundServiceAPI21.class);
JobInfo.Builder builder = new JobInfo.Builder(1, service).setPersisted(true)
    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).setRequiresCharging(false);
jobScheduler.schedule(builder.build());
            jobScheduler.schedule(builder.build()); //It runs when I call this - but doesn't re-run if the network changes

Manifest (Abstract):

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
    <uses-permission android:name="android.permission.VIBRATE" />

    <application>
        <service
            android:name=".services.ConnectivityBackgroundServiceAPI21"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE" />
    </application>
</manifest>

I guess there has to be an easy solution to this but I am not able to find it.

Ch4t4r
  • 1,387
  • 1
  • 11
  • 30
  • If you are unwilling to run a service in foreground then you are correct you cannot use the connectivity manager. What specifically are you trying to do when the connectivity changes? – tyczj Sep 11 '17 at 19:38
  • I'm trying to start a VPNService (or an Activity where the user can configure it) – Ch4t4r Sep 11 '17 at 19:41
  • You're using the `registerNetworkCallback()` with the `PendingIntent`, not the `NetworkCallback` object? – Pablo Baxter Sep 11 '17 at 22:55
  • I've been using it with the callback. The call with the PendingIntent looks very promising, I'll have a look into it. Thanks! – Ch4t4r Sep 12 '17 at 04:06
  • 1
    Firebase Job Dispatcher ia retrocompatible so you dont have to write and support code for different versions https://github.com/firebase/firebase-jobdispatcher-android – cutiko Sep 12 '17 at 18:11
  • Thanks, I'll have a look. For another rather interesting finding look into my edited answer :) – Ch4t4r Sep 12 '17 at 18:21
  • I'm now using the firebase JobDispatcher. Thanks for bringing that to my attention. It works like a charm and I've expanded my answer with this knowledge. – Ch4t4r Sep 12 '17 at 19:11

2 Answers2

13

Second edit: I'm now using firebase's JobDispatcher and it works perfect across all platforms (thanks @cutiko). This is the basic structure:

public class ConnectivityJob extends JobService{

    @Override
    public boolean onStartJob(JobParameters job) {
        LogFactory.writeMessage(this, LOG_TAG, "Job created");
        connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            connectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), networkCallback = new ConnectivityManager.NetworkCallback(){
                // -Snip-
            });
        }else{
            registerReceiver(connectivityChange = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    handleConnectivityChange(!intent.hasExtra("noConnectivity"), intent.getIntExtra("networkType", -1));
                }
            }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
        }

        NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
        if (activeNetwork == null) {
            LogFactory.writeMessage(this, LOG_TAG, "No active network.");
        }else{
            // Some logic..
        }
        LogFactory.writeMessage(this, LOG_TAG, "Done with onStartJob");
        return true;
    }


    @Override
    public boolean onStopJob(JobParameters job) {
        if(networkCallback != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)connectivityManager.unregisterNetworkCallback(networkCallback);
        else if(connectivityChange != null)unregisterReceiver(connectivityChange);
        return true;
    }

    private void handleConnectivityChange(NetworkInfo networkInfo){
        // Calls handleConnectivityChange(boolean connected, int type)
    }

    private void handleConnectivityChange(boolean connected, int type){
        // Calls handleConnectivityChange(boolean connected, ConnectionType connectionType)
    }

    private void handleConnectivityChange(boolean connected, ConnectionType connectionType){
        // Logic based on the new connection
    }

    private enum ConnectionType{
        MOBILE,WIFI,VPN,OTHER;
    }
}

I call it like so (in my boot receiver):

Job job = dispatcher.newJobBuilder()
                    .setService(ConnectivityJob.class)
                    .setTag("connectivity-job")
                    .setLifetime(Lifetime.FOREVER)
                    .setRetryStrategy(RetryStrategy.DEFAULT_LINEAR)
                    .setRecurring(true)
                    .setReplaceCurrent(true)
                    .setTrigger(Trigger.executionWindow(0, 0))
                    .build();

Edit: I found a hacky way. Really hacky to say. It works but I wouldn't use it:

  • Start a foreground service, go into foreground mode using startForeground(id, notification) and use stopForeground after that, the user won't see the notification but Android registers it as having been in the foreground
  • Start a second service, using startService
  • Stop the first service
  • Result: Congratulations, you have a service running in the background (the one you started second).
  • onTaskRemoved get's called on the second service when you open the app and it is cleared from RAM, but not when the first service terminates. If you have an repeating action like a handler and don't unregister it in onTaskRemoved it continues to run.

Effectively this starts a foreground service, which starts a background service and then terminates. The second service outlives the first one. I'm not sure whether this is intended behavior or not (maybe a bug report should be filed?) but it's a workaround (again, a bad one!).


Looks like it's not possible to get notified when the connection changes:

  • With Android 7.0 CONNECTIVITY_ACTION receivers declared in the manifest won't receive broadcasts. Additionally receivers declared programmatically only receive the broadcasts if the receiver was registered on the main thread (so using a service won't work). If you still want to receive updates in the background you can use connectivityManager.registerNetworkCallback
  • With Android 8.0 the same restrictions are in place but in addition you can't launch services from the background unless it's a foreground service.

All of this allows these solutions:

  • Start a foreground service and show a notification
    • This most likely bothers users
  • Use JobService and schedule it to run periodically
    • Depending on your setting it takes some time until the service get's called thus a few seconds could have passed since the connection changes. All in all this delays the action which should happen on connection change

This is not possible:

  • connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent) cannot be used because the PendingIntent executes it's action instantly if the condition is met; it's only called once
    • Trying to start a foreground service this way which goes to the foreground for 1 ms and re-registers the call results in something comparable to infinite recursion

Since I already have a VPNService which runs in foreground mode (and thus shows a notification) I implemented that the Service to check connectivity runs in foreground if the VPNService doesn't and vice-versa. This always shows a notification, but only one.
All in all I find the 8.0 update extremely unsatisfying, it seems like I either have to use unreliable solutions (repeating JobService) or interrupt my users with a permanent notification. Users should be able to whitelist apps (The fact alone that there are apps which hide the "XX is running in background" notification should say enough). So much for off-topic

Feel free to expand my solution or show me any errors, but these are my findings.

JJD
  • 50,076
  • 60
  • 203
  • 339
Ch4t4r
  • 1,387
  • 1
  • 11
  • 30
  • Regarding your hacky solution, Did you test that the service was running for longer duration (24 H) in the background ? I have tried another hacky solution which was to run a worker thread started by the mainThread. This thread would do network monitoring periodically (20 sec interval). However, upon testing for longer duration I found that the system terminated the entire process depending on system resource requirements or sometimes just like that. – shailesh mota Oct 27 '17 at 09:08
  • I didn't test whether it runs over a long period of time, but the system always terminates any service which isn't a foreground service if the system is running low on resources. In some cases you can just ignore calls to onTerminate() which would normally be used to free references and stop on-going operations which might result in the service running forever. I'd advise you to test it though. The most important fact for me was that I can launch a service when being in the background. – Ch4t4r Oct 27 '17 at 09:18
  • You set job dispatcher trigger immediately after calling in boot receiver. Then what is need of job dispatcher? You may call the service from the boot receiver. – Sourav Bagchi Nov 20 '17 at 13:50
  • No, I may not. The service is running in the background and thus cannot be started from the BootReceiver. You can only start a background service from a foreground service/activity. The whole point is that I constantly need to monitor the connection state (or at least get notified of changes) which requires a service to listen to it. As I cannot start a normal service I resort to JobService. – Ch4t4r Nov 20 '17 at 14:01
  • im using this code and after 12 hours working, now my jobservice seems stopped, any idea what can be wrong? – Pablo Cegarra Apr 12 '18 at 11:02
  • The problem is that I don't want to use Firebase. Also, Play Services need to be installed for that. Do you have another solution in the meantime? – mbo Sep 20 '18 at 07:57
  • Not really, if you want to support older android versions as well. You could of course write your own class which either starts the service normall or as a job, depending on the Android version. You could use this hack as well, but I don't recommend it: https://stackoverflow.com/questions/51289236/continually-running-background-service/51485873#51485873. You'd have to modify it a lot of course. – Ch4t4r Sep 20 '18 at 17:30
  • @Ch4t4r - Using your FirebaseJobDispatcher approach, I'm seeing the connectivity changes are only received while your Job is running, which is typically just 4-6 minutes but then stopped. The Job is often not started again for several minutes, typically 3-5 but sometimes >10mins, so no connection changes are received during these long periods. You must be seeing the same thing right? – LBC Feb 08 '19 at 00:47
2

i've have to add some modifications to Ch4t4r jobservice and work for me

public class JobServicio extends JobService {

    String LOG_TAG ="EPA";
    ConnectivityManager.NetworkCallback networkCallback;
    BroadcastReceiver connectivityChange;
    ConnectivityManager connectivityManager;

    @Override
    public boolean onStartJob(JobParameters job) {
        Log.i(LOG_TAG, "Job created");
        connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            connectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), networkCallback = new ConnectivityManager.NetworkCallback(){
                // -Snip-
            });
        }else{
            registerReceiver(connectivityChange = new BroadcastReceiver() { //this is not necesary if you declare the receiver in manifest and you using android <=6.0.1
                @Override
                public void onReceive(Context context, Intent intent) {
                    Toast.makeText(context, "recepcion", Toast.LENGTH_SHORT).show();
                    handleConnectivityChange(!intent.hasExtra("noConnectivity"), intent.getIntExtra("networkType", -1));
                }
            }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
        }


        Log.i(LOG_TAG, "Done with onStartJob");
        return true;
    }
    @Override
    public boolean onStopJob(JobParameters job) {
        if(networkCallback != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)connectivityManager.unregisterNetworkCallback(networkCallback);
        else if(connectivityChange != null)unregisterReceiver(connectivityChange);
        return true;
    }
    private void handleConnectivityChange(NetworkInfo networkInfo){
        // Calls handleConnectivityChange(boolean connected, int type)
    }
    private void handleConnectivityChange(boolean connected, int type){
        // Calls handleConnectivityChange(boolean connected, ConnectionType connectionType)
        Toast.makeText(this, "erga", Toast.LENGTH_SHORT).show();
    }
    private void handleConnectivityChange(boolean connected, ConnectionType connectionType){
        // Logic based on the new connection
    }
    private enum ConnectionType{
        MOBILE,WIFI,VPN,OTHER;
    }

    ConnectivityManager.NetworkCallback x = new ConnectivityManager.NetworkCallback() { //this networkcallback work wonderfull

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onAvailable(Network network) {
            Log.d(TAG, "requestNetwork onAvailable()");
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                //do something
            }
            else {
                //This method was deprecated in API level 23
                ConnectivityManager.setProcessDefaultNetwork(network);
            }
        }

        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
            Log.d(TAG, ""+network+"|"+networkCapabilities);
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
            Log.d(TAG, "requestNetwork onLinkPropertiesChanged()");
        }

        @Override
        public void onLosing(Network network, int maxMsToLive) {
            Log.d(TAG, "requestNetwork onLosing()");
        }

        @Override
        public void onLost(Network network) {
        }
    }
}

and you can call the jobservice from another normal service or boot_complete receiver

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
    Job job = dispatcher.newJobBuilder()
                        .setService(Updater.class)
                        .setTag("connectivity-job")
                        .setLifetime(Lifetime.FOREVER)
                        .setRetryStrategy(RetryStrategy.DEFAULT_LINEAR)             
                        .setRecurring(true)
                        .setReplaceCurrent(true)
                        .setTrigger(Trigger.executionWindow(0, 0))
                        .build();
    dispatcher.mustSchedule(job);
}

in manifest...

<service
    android:name=".Updater"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true"/>
JJD
  • 50,076
  • 60
  • 203
  • 339