【问题标题】:Android O - Detect connectivity change in backgroundAndroid O - 在后台检测连接变化
【发布时间】:2017-09-11 19:30:57
【问题描述】:

首先:我知道ConnectivityManager.CONNECTIVITY_ACTION 已被弃用,我知道如何使用connectivityManager.registerNetworkCallback。此外,如果阅读有关 JobScheduler 的信息,但我不完全确定我是否正确。

我的问题是我想在手机连接/断开网络时执行一些代码。当应用程序在后台时也应该发生这种情况。从 Android OI 开始,如果我想在后台运行我想要避免的服务,则必须显示通知。
我尝试使用 JobScheduler/JobService API 获取有关手机何时连接/断开连接的信息,但它只在我安排它的时间执行。对我来说,当这样的事件发生时,我似乎无法运行代码。有没有办法实现这一目标?我可能只需要稍微调整一下我的代码吗?

我的工作服务:

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

我想必须有一个简单的解决方案,但我找不到它。

【问题讨论】:

  • 如果您不愿意在前台运行服务,那么您是正确的,您不能使用连接管理器。当连接发生变化时,您具体要做什么?
  • 我正在尝试启动 VPNService(或用户可以配置它的 Activity)
  • 您将registerNetworkCallback()PendingIntent 一起使用,而不是NetworkCallback 对象?
  • 我一直在回调中使用它。带有 PendingIntent 的调用看起来很有希望,我会研究一下。谢谢!
  • Firebase Job Dispatcher 具有逆向兼容性,因此您不必为不同版本编写和支持代码github.com/firebase/firebase-jobdispatcher-android

标签: android android-service android-8.0-oreo


【解决方案1】:

第二次编辑:我现在正在使用 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 方法。说起来真的很鸡肋。它有效,但我不会使用它:

  • 启动前台服务,使用 startForeground(id, notification) 进入前台模式,然后使用stopForeground,用户将看不到通知,但 Android 将其注册为已在前台
  • 启动第二个服务,使用startService
  • 停止第一个服务
  • 结果:恭喜,您在后台运行了一项服务(您第二次启动的服务)。
  • onTaskRemoved 在您打开应用程序并从 RAM 中清除时在第二个服务上调用,但在第一个服务终止时不会。如果您有一个像处理程序这样的重复操作并且没有在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 秒间隔)进行网络监控。但是,经过较长时间的测试,我发现系统终止了整个进程,具体取决于系统资源需求,或者有时只是这样。
  • 我没有测试它是否运行了很长时间,但是如果系统资源不足,系统总是会终止任何不是前台服务的服务。在某些情况下,您可以忽略对 onTerminate() 的调用,这通常用于释放引用并停止正在进行的操作,这可能导致服务永远运行。不过我建议你测试一下。对我来说最重要的事实是我可以在后台启动服务。
  • 您在调用启动接收器后立即设置作业调度程序触发器。那么工作调度员需要什么?您可以从引导接收器调用该服务。
  • 不,我可能不会。该服务在后台运行,因此无法从 BootReceiver 启动。您只能从前台服务/活动启动后台服务。重点是我经常需要监视连接状态(或至少收到更改通知),这需要服务来监听它。由于我无法启动正常的服务,我求助于 JobService。
  • 我正在使用此代码,工作 12 小时后,现在我的作业服务似乎停止了,知道有什么问题吗?
【解决方案2】:

我必须对 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) {
        }
    }
}

您可以从另一个普通服务或 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);
}

在清单中...

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-03
    • 1970-01-01
    • 2016-01-24
    • 1970-01-01
    • 1970-01-01
    • 2016-03-09
    • 2012-09-14
    相关资源
    最近更新 更多