web-dev-qa-db-fra.com

Android O - Détecter le changement de connectivité en arrière-plan

Tout d'abord: je sais que ConnectivityManager.CONNECTIVITY_ACTION est obsolète et je sais comment utiliser connectivityManager.registerNetworkCallback. En outre, si vous lisez à propos de JobScheduler, mais je ne suis pas tout à fait sûr si j'ai bien compris.

Mon problème est que je veux exécuter du code lorsque le téléphone est connecté/déconnecté d'un réseau. Cela devrait se produire lorsque l'application est également à l'arrière-plan. À partir d'Android O, je devrais afficher une notification si je veux exécuter un service en arrière-plan, ce que je veux éviter.
J'ai essayé d'obtenir des informations sur le moment où le téléphone se connecte/déconnecte à l'aide des API JobScheduler/JobService, mais il n'est exécuté que lorsque je le planifie. Pour moi, il semble que je ne peux pas exécuter de code lorsqu'un événement comme celui-ci se produit. Y'a-t-il une quelconque façon de réussir cela? Dois-je peut-être juste ajuster un peu mon code?

Mon 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;
}

Je lance le service comme suit:

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

Manifeste (Résumé):

<?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>

Je suppose qu'il doit y avoir une solution simple à ce problème, mais je ne parviens pas à le trouver.

8
Ch4t4r

Deuxième édition: J'utilise maintenant JobDispatcher de Firebase et cela fonctionne parfaitement sur toutes les plateformes (merci @cutiko). C'est la structure de base:

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;
    }
}

Je l'appelle comme ça (dans mon récepteur d'amorçage):

   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: J'ai trouvé un moyen hacky. Vraiment hacky à dire. Cela fonctionne mais je ne l'utiliserais pas:

  • Démarrer un service de premier plan, passer en mode de premier plan à l'aide de startForeground (id, notification) et utiliser stopForeground après cela, l'utilisateur ne verra pas la notification, mais Android l'enregistrera comme étant au premier plan.
  • Démarrer un deuxième service en utilisant startService 
  • Arrêtez le premier service
  • Résultat: Félicitations, vous avez un service exécuté en arrière-plan (celui que vous avez démarré en deuxième). 
  • onTaskRemoved est appelé sur le deuxième service lorsque vous ouvrez l'application et il est effacé de la RAM, mais pas lorsque le premier service se termine. Si vous avez une action répétitive comme un gestionnaire et que vous ne la désenregistrez pas dans onTaskRemoved, elle continue de s'exécuter.

Effectivement, cela démarre un service de premier plan, qui démarre un service en arrière-plan, puis se termine. Le deuxième service survit au premier. Je ne suis pas sûr que ce soit le comportement voulu ou pas (peut-être un rapport de bogue devrait être classé?) Mais c'est une solution de contournement (encore une fois, un mauvais!).


On dirait qu'il n'est pas possible d'être averti lorsque la connexion change:

  • Avec Android 7.0, CONNECTIVITY_ACTION, les récepteurs déclarés dans le manifeste ne recevront aucune émission. De plus, les récepteurs déclarés par programme ne reçoivent les émissions que si le récepteur a été enregistré sur le thread principal (l’utilisation d’un service ne fonctionnera donc pas). Si vous souhaitez toujours recevoir des mises à jour en arrière-plan, vous pouvez utiliser connectivityManager.registerNetworkCallback 
  • Avec Android 8.0, les mêmes restrictions sont en place, mais vous ne pouvez pas non plus lancer de services en arrière-plan, sauf s'il s'agit d'un service de premier plan.

Tout cela permet ces solutions:

  • Démarrer un service de premier plan et afficher une notification
    • Cela dérange probablement les utilisateurs
  • Utilisez JobService et planifiez-le de s'exécuter périodiquement
    • En fonction de votre configuration, il faut un certain temps avant que le service soit appelé. Ainsi, quelques secondes pourraient s'être écoulées depuis la modification de la connexion. Dans l’ensemble, cela retarde l’action à entreprendre lors du changement de connexion

Ce n'est pas possible:

  • connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent) ne peut pas être utilisé car PendingIntent exécute son action instantanément si la condition est remplie; ça ne s'appelle qu'une fois
    • Essayer de démarrer un service de premier plan de cette façon, qui passe au premier plan pendant 1 ms et enregistre à nouveau l’appel, donne un résultat comparable à la récursion infinie.

Comme j'ai déjà un service VPN qui fonctionne en mode de premier plan (et affiche donc une notification), j'ai implémenté que le service de vérification de la connectivité s'exécute en premier plan si VPNService ne fonctionne pas et vice-versa. Cela montre toujours une notification, mais une seule.
Globalement, je trouve la mise à jour 8.0 extrêmement insatisfaisante. Il semble que je dois soit utiliser des solutions peu fiables (répéter JobService), soit interrompre mes utilisateurs avec une notification permanente. Les utilisateurs doivent pouvoir ajouter des applications à la liste blanche (le seul fait qu'il existe des applications qui masquent la notification "XX s'exécute en arrière-plan" devrait en dire assez). Voilà pour hors-sujet

N'hésitez pas à développer ma solution ou à me montrer des erreurs, mais ce sont mes conclusions.

8
Ch4t4r

je dois ajouter quelques modifications au service d'emploi Ch4t4r et travailler pour moi

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) {
}
    }
    }

et vous pouvez appeler le service emplois à partir d'un autre service normal ou d'un récepteur boot_complete

        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);
        }

en manifeste ...

    <service
        Android:name=".Updater"
        Android:permission="Android.permission.BIND_JOB_SERVICE"
        Android:exported="true"/>