webentwicklung-frage-antwort-db.com.de

Android O - Erkennung von Verbindungsänderungen im Hintergrund

Zunächst einmal: Ich weiß, dass ConnectivityManager.CONNECTIVITY_ACTION veraltet ist und ich weiß, wie connectivityManager.registerNetworkCallback verwendet wird. Außerdem, wenn Sie über JobScheduler gelesen haben, aber ich bin mir nicht ganz sicher, ob ich es richtig verstanden habe.

Mein Problem ist, dass ich einen Code ausführen möchte, wenn das Telefon mit einem Netzwerk verbunden ist oder nicht. Dies sollte passieren, wenn sich die App auch im Hintergrund befindet. Beginnend mit Android O muss ich eine Benachrichtigung anzeigen, wenn ich im Hintergrund einen Dienst ausführen möchte, was ich vermeiden möchte.
Ich habe versucht, Informationen zu erhalten, wenn das Telefon die JobScheduler/JobService-APIs verwendet/trennt, aber es wird nur ausgeführt, wenn ich es plane. Für mich scheint es, als könnte ich keinen Code ausführen, wenn ein solches Ereignis passiert. Gibt es eine Möglichkeit, dies zu erreichen? Muss ich meinen Code vielleicht nur ein bisschen anpassen?

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

Ich starte den Dienst 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 (Zusammenfassung):

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

Ich denke, es muss eine einfache Lösung dafür geben, aber ich kann sie nicht finden.

8
Ch4t4r

Zweite Änderung: Ich benutze jetzt den JobDispatcher von firebase und funktioniert perfekt auf allen Plattformen (danke @cutiko). Dies ist die Grundstruktur:

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

Ich nenne es so (in meinem 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: Ich habe einen gehackten Weg gefunden. Wirklich hackig zu sagen. Es funktioniert, aber ich würde es nicht verwenden:

  • Starten Sie einen Vordergrunddienst, wechseln Sie mit startForeground (id, notification) in den Vordergrundmodus und verwenden Sie anschließend stopForeground. Der Benutzer sieht die Benachrichtigung nicht, aber Android registriert sie als im Vordergrund
  • Starten Sie einen zweiten Dienst mit startService 
  • Stoppen Sie den ersten Dienst
  • Ergebnis: Glückwunsch, Sie haben einen Dienst im Hintergrund (den Dienst, den Sie als zweiter gestartet haben). 
  • onTaskRemoved wird beim zweiten Dienst aufgerufen, wenn Sie die App öffnen, und es wird aus dem RAM gelöscht, nicht jedoch, wenn der erste Dienst beendet wird. Wenn Sie eine sich wiederholende Aktion wie einen Handler haben und die Registrierung in onTaskRemoved nicht aufheben, wird sie weiter ausgeführt.

Dies startet effektiv einen Vordergrunddienst, der einen Hintergrunddienst startet und dann beendet wird. Der zweite Dienst überlebt den ersten. Ich bin nicht sicher, ob dies beabsichtigt ist oder nicht (vielleicht sollte ein Fehlerbericht eingereicht werden?), Aber es ist eine Problemumgehung (wieder eine schlechte!).


Anscheinend ist es nicht möglich, benachrichtigt zu werden, wenn sich die Verbindung ändert:

  • Mit Android 7.0 CONNECTIVITY_ACTION-Empfängern, die im Manifest deklariert sind, werden keine Broadcasts empfangen. Darüber hinaus empfangen programmgesteuert deklarierte Empfänger die Broadcasts nur, wenn der Empfänger im Hauptthread registriert wurde (die Verwendung eines Dienstes funktioniert daher nicht). Wenn Sie weiterhin Updates im Hintergrund erhalten möchten, können Sie connectivityManager.registerNetworkCallback verwenden. 
  • Mit Android 8.0 gelten die gleichen Einschränkungen. Außerdem können Sie Dienste nicht aus dem Hintergrund starten, es sei denn, es handelt sich um einen Vordergrunddienst.

All dies ermöglicht diese Lösungen:

  • Starten Sie einen Vordergrunddienst und zeigen Sie eine Benachrichtigung an
    • Dies stört höchstwahrscheinlich die Benutzer
  • Verwenden Sie JobService und planen Sie, dass es regelmäßig ausgeführt wird
    • Abhängig von Ihrer Einstellung dauert es einige Zeit, bis der Dienst aufgerufen wird. Daher können einige Sekunden vergangen sein, seit die Verbindung geändert wurde. Alles in allem verzögert dies die Aktion, die bei einem Verbindungswechsel stattfinden sollte

Das ist nicht möglich:

  • connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent) kann nicht verwendet werden, da das PendingIntent seine Aktion sofort ausführt, wenn die Bedingung erfüllt ist. es wird nur einmal aufgerufen
    • Wenn Sie versuchen, einen Vordergrunddienst auf diese Weise zu starten, der für 1 ms in den Vordergrund geht und den Anruf erneut registriert, führt dies zu einer mit der unendlichen Rekursion vergleichbaren Funktion

Da ich bereits einen VPNService habe, der im Vordergrundmodus läuft (und somit eine Benachrichtigung anzeigt), habe ich implementiert, dass der Dienst zur Überprüfung der Konnektivität im Vordergrund ausgeführt wird, falls dies nicht der Fall ist, und umgekehrt. Dies zeigt immer eine Benachrichtigung, aber nur eine.
Alles in allem finde ich das 8.0-Update äußerst unbefriedigend. Es scheint, als müsste ich entweder unzuverlässige Lösungen verwenden (JobService wiederholen) oder meine Benutzer mit einer permanenten Benachrichtigung unterbrechen. Benutzer sollten in der Lage sein, Apps auf die Whitelist zu setzen (Allein die Tatsache, dass es Apps gibt, die die Benachrichtigung "XX wird im Hintergrund ausgeführt" verdeckt, sollte genug sagen. So viel zum Off-Topic

Fühlen Sie sich frei, meine Lösung zu erweitern oder mir Fehler zu zeigen, aber dies sind meine Ergebnisse.

8
Ch4t4r

ich muss einige Änderungen an Ch4t4r Jobservice hinzufügen und für mich arbeiten

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

und Sie können den Jobservice von einem anderen normalen Dienst oder boot_complete-Empfänger aus aufrufen

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

im Manifest ...

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