Android O-обнаружение изменения подключения в фоновом режиме

во-первых: я знаю, что ConnectivityManager.CONNECTIVITY_ACTION устарел и я знаю, как использовать connectivityManager.registerNetworkCallback. Кроме того, если читать о JobScheduler, но я не совсем уверен, правильно ли я понял.

моя проблема в том, что я хочу выполнить некоторый код, когда телефон подключен / отключен от сети. Это должно произойти, когда приложение находится в фоновом режиме. Начиная с Android O мне нужно будет показать уведомление, если я хочу запустить службу в фоновом режиме, что я хочу избегать.
я попытался получить информацию о том, когда телефон подключается / отключается с помощью JobScheduler/JobService APIs, но он выполняется только в то время, когда я его планирую. Мне кажется, что я не могу запустить код, когда такое событие произойдет. Есть ли способ достичь этого? Может быть, мне просто нужно немного изменить свой код?

Мой 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;
}

Я начинаю службу так:

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

Манифест (Тезисы):

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

Я думаю, что должно быть легко решение этого, но я не могу найти его.

2 ответов


второе редактирование: теперь я использую jobdispatcher firebase, и он отлично работает на всех платформах (спасибо @cutiko). Это основная структура:

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

я называю это так (в моем загрузочный приемник):

   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: я нашел хакерский способ. Очень банально сказать. Он работает, но я бы не использовал его:

  • запустите службу переднего плана, перейдите в режим переднего плана с помощью startForeground (id, notification) и используйте stopForeground после этого пользователь не будет видеть уведомление, но Android регистрирует его как находящийся на переднем плане
  • запустите вторую службу, используя startService
  • остановить первую службу
  • результат: Поздравляем, у вас есть служба работает в фоновом режиме (тот, который вы начали второй).
  • onTaskRemoved get вызывается второй службой при открытии приложения, и она очищается от ОЗУ, но не при завершении первой службы. Если у вас есть повторяющееся действие как обработчик и не отменяйте регистрацию в onTaskRemoved он продолжает работать.

эффективно это запускает службу переднего плана, которая запускает фоновую службу, а затем завершается. Вторая служба переживает первую. Я не уверен, является ли это намеренным поведением или нет (возможно, отчет об ошибке должен быть подан?) но это обходной путь (опять же, плохой!).


похоже, что невозможно получить уведомление, когда соединение изменения:

  • С Android 7.0 CONNECTIVITY_ACTION приемники, объявленные в манифесте, не будут получать трансляции. Кроме того, приемники, объявленные программно, получают трансляции, только если приемник был зарегистрирован в основном потоке (поэтому использование службы не будет работать). Если вы все еще хотите получать обновления в фоновом режиме, вы можете использовать connectivityManager.registerNetworkCallback
  • С Android 8.0 действуют те же ограничения, но, кроме того, вы не можете запускать службы из фона если только это не служба переднего плана.

все это позволяет эти решения:

  • запустите службу переднего плана и покажите уведомление
    • это скорее всего беспокоит пользователей
  • используйте JobService и запланируйте его для периодического запуска
    • в зависимости от вашей настройки требуется некоторое время, пока служба get не будет вызвана, таким образом, через несколько секунд после изменения соединения. Все это задерживает действие, которое должно произойти при изменении соединения

это невозможно:

  • connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent) не может использоваться, потому что PendingIntent выполняет его действие мгновенно, если условие выполнено; он вызывается только один раз
    • попытка запустить службу переднего плана таким образом, который выходит на передний план для 1 мс и повторно регистрирует результаты вызова в чем-то сопоставимом с бесконечным рекурсия

поскольку у меня уже есть VPNService, который работает в режиме переднего плана (и, таким образом, показывает уведомление), я реализовал, что Служба проверки подключения работает на переднем плане, если VPNService этого не делает, и наоборот. Это всегда показывает уведомление, но только один.
в целом я нахожу обновление 8.0 крайне неудовлетворительным, похоже, мне либо приходится использовать ненадежные решения (повторяя JobService), либо прерывать моих пользователей постоянным уведомление. Пользователи должны иметь возможность белый список приложений (тот факт, что есть приложения, которые скрывают "XX работает в фоновом режиме" уведомление должно сказать достаточно). Так много для off-topic

Не стесняйтесь расширять мое решение или показывать мне какие-либо ошибки, но это мои выводы.


Я должен добавить некоторые изменения в Ch4t4r jobservice и работать на меня

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

и вы можете вызвать службу jobservice из другой обычной службы или 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);
        }

в манифесте...

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