Android O - 后台检测网络连接变化

16
首先,我知道ConnectivityManager.CONNECTIVITY_ACTION已经被弃用,并且我知道如何使用connectivityManager.registerNetworkCallback。此外,我也了解JobScheduler,但我不确定自己是否理解正确。
我的问题是,当手机连接/断开网络时,我想执行一些代码。这应该在应用程序处于后台时发生。从Android O开始,如果要在后台运行服务,必须显示通知,而我想避免这种情况。我尝试使用JobScheduler/JobService API获取手机连接/断开连接的信息,但它只在我安排它的时间执行。对我来说,似乎无法在此类事件发生时运行代码。有没有办法实现这一点?我可能只需要稍微调整一下我的代码吗?
我的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>

我想这个问题一定有简单的解决方案,但我找不到。


如果您不想在前台运行服务,那么您是正确的,您不能使用连接管理器。当连接发生变化时,您具体想要做什么? - tyczj
我正在尝试启动一个VPNService(或者一个用户可以配置它的Activity)。 - Ch4t4r
你是在使用 registerNetworkCallback() 方法时,传入了 PendingIntent 参数而不是 NetworkCallback 对象吗? - Pablo Baxter
1
Firebase Job Dispatcher是向后兼容的,因此您不必为不同版本编写和支持代码。https://github.com/firebase/firebase-jobdispatcher-android - cutiko
谢谢,我会看一下。如果你想看到另一个有趣的发现,请查看我的编辑答案 :) - Ch4t4r
显示剩余2条评论
2个回答

13

第二次编辑:我现在正在使用Firebase的JobDispatcher,在所有平台上都表现得非常完美(感谢@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();

编辑:我找到了一个hacky方法,非常hacky,但它确实有效。但我不会使用它:

  • 启动一个前台服务,使用startForeground(id, notification)进入前台模式,然后使用stopForeground,用户看不到通知,但Android将其注册为前台
  • 启动第二个服务,使用startService
  • 停止第一个服务
  • 结果:恭喜你,你有一个在后台运行的服务(您启动的第二个服务)。
  • 当您打开应用程序并从RAM中清除时,在第二个服务上调用onTaskRemoved,但当第一个服务终止时不会调用。如果您有像处理程序这样的重复操作,并且在onTaskRemoved中未注销它,则它将继续运行。

实际上,这启动了一个前台服务,该服务启动了一个后台服务,然后终止。第二个服务比第一个服务更长寿。我不确定这是否是预期行为(也许应该提出错误报告?),但这是一种解决方法(再次强调,不好!)。


看起来不可能在连接更改时收到通知:

  • 对于Android 7.0,声明在清单中的CONNECTIVITY_ACTION接收器将不会收到广播。此外,仅在主线程上注册了程序化声明的接收器才能接收广播(因此使用服务无法工作)。如果您仍然希望在后台接收更新,则可以使用connectivityManager.registerNetworkCallback
  • 对于Android 8.0,相同的限制也存在,但是除此之外,除非它是前台服务,否则无法从后台启动服务。

所有这些都允许以下解决方案:

  • 启动前台服务并显示通知
    • 这很可能会打扰用户
  • 使用JobService并安排定期运行
    • 根据您的设置,直到服务被调用需要一些时间,因此在连接更改时可能已经过去了几秒钟。总的来说,这会延迟应该在连接更改时发生的操作

这是不可能的:

  • connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent)不能使用,因为PendingIntent立即执行其动作(如果条件满足);它只调用一次。
    • 试图以这种方式启动前台服务,使其在1毫秒内进入前台并重新注册调用的结果类似于无限递归

由于我已经有一个运行在前台模式下的VPNService(因此显示通知),所以如果VPNService没有运行,则将服务检查连接运行在前台,反之亦然。这总是显示一个通知,但只显示一个。
总的来说,我认为8.0更新非常不令人满意,似乎我必须使用不可靠的解决方案(重复的JobService)或通过永久通知打断我的用户。用户应该能够将应用程序列入白名单(仅有许多隐藏“XX在后台运行”的通知的应用程序就足以说明问题)。至此为止,就是离题了。

欢迎扩展我的解决方案或向我展示任何错误,但这些是我发


关于你的hacky解决方案,你测试过服务在后台运行更长时间(24小时)吗?我尝试了另一种hacky解决方案,即运行由mainThread启动的工作线程。该线程将定期进行网络监视(20秒间隔)。然而,在长时间测试后,我发现系统会根据系统资源需求或有时仅仅是这样终止整个进程。 - shailesh mota
我没有测试它是否能够长时间运行,但是如果系统资源不足,系统会终止任何不是前台服务的服务。在某些情况下,您可以忽略对onTerminate()的调用,这通常用于释放引用和停止正在进行的操作,否则可能导致服务永远运行。虽然我建议您进行测试。对我来说最重要的事实是,我可以在后台启动服务。 - Ch4t4r
问题是我不想使用Firebase。此外,为此需要安装Play服务。在此期间,你有其他解决方案吗? - mbo
如果你想支持旧版的Android系统,那么这并不是一个好方法。当然,你可以编写自己的类,根据Android版本启动服务或作为任务运行。你也可以使用这个hack,但我不建议这样做:https://dev59.com/eFUK5IYBdhLWcg3wySbB#51485873。当然,你需要进行大量修改。 - Ch4t4r
@Ch4t4r - 使用您的FirebaseJobDispatcher方法,我发现连接更改仅在作业运行时接收到,这通常只有4-6分钟,但然后停止。该作业通常在几分钟内不会再次启动,通常为3-5分钟,但有时> 10分钟,因此在这些长时间段内不会接收到任何连接更改。您也必须看到同样的情况,对吗? - LBC
显示剩余3条评论

2

我需要对Ch4t4r任务服务进行一些修改,以适应我的需求。

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

您可以从另一个普通服务或boot_complete接收器调用jobservice

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"/>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接