Android O - 在后台检测连接变化
Android O - Detect connectivity change in background
首先:我知道 ConnectivityManager.CONNECTIVITY_ACTION
已被弃用,我知道如何使用 connectivityManager.registerNetworkCallback
。此外,如果阅读 JobScheduler
,但我不完全确定我是否理解正确。
我的问题是我想在 phone 连接/断开 to/from 网络时执行一些代码。当应用程序在后台时也应该发生这种情况。从 Android O 开始,如果我想 运行 我想避免的后台服务,我必须显示通知。
我尝试获取有关 phone 使用 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>
我想一定有一个简单的解决方案,但我找不到。
第二次编辑:我现在正在使用 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
- 停止第一个服务
- 结果:恭喜,您在后台有一个服务 运行ning(您第二次启动的服务)。
- onTaskRemoved 会在您打开应用程序时在第二个服务上调用,并且它会从 RAM 中清除,但不会在第一个服务终止时调用。如果您有像处理程序这样的重复操作并且不在
onTaskRemoved
中注销它,它会继续到 运行。
实际上这会启动一个前台服务,它会启动一个后台服务,然后终止。第二个服务比第一个服务长。我不确定这是否是预期的行为(也许应该提交错误报告?)但这是一种解决方法(同样,一个糟糕的方法!)。
似乎无法在连接更改时收到通知:
- 使用 Android 7.0
CONNECTIVITY_ACTION
清单中声明的接收器将不会接收广播。此外,如果接收器已在主线程上注册,则以编程方式声明的接收器仅会接收广播(因此使用服务将不起作用)。如果您仍想在后台接收更新,可以使用 connectivityManager.registerNetworkCallback
- 在 Android 8.0 中存在相同的限制,但除此之外,您无法从后台启动服务,除非它是前台服务。
所有这些都允许这些解决方案:
- 启动前台服务并显示通知
- 这很可能会困扰用户
- 使用 JobService 并定期将其安排到 运行
- 根据您的设置,调用服务需要一些时间,因此连接更改后可能已经过了几秒钟。总而言之,这会延迟连接更改时应该发生的操作
这是不可能的:
connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent)
不能使用,因为如果满足条件,PendingIntent 会立即执行它的操作;它只被调用一次
- 尝试以这种方式启动前台服务,该服务进入前台 1 毫秒并重新注册调用,结果类似于无限递归
因为我已经有一个 运行 处于前台模式的 VPNService(因此显示了一个通知)我实施了该服务来检查前台的连接性 运行s 如果 VPNService 没有反之亦然。这总是显示一个通知,但只有一个。
总之,我发现 8.0 更新非常不令人满意,似乎我要么必须使用不可靠的解决方案(重复 JobService),要么用永久通知打断我的用户。用户应该能够将应用程序列入白名单(仅凭存在隐藏 "XX is running in background" 通知的应用程序这一事实就足够了)。题外话就这么多
请随意扩展我的解决方案或向我展示任何错误,但这些是我的发现。
我必须对 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 接收方调用 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"/>
首先:我知道 ConnectivityManager.CONNECTIVITY_ACTION
已被弃用,我知道如何使用 connectivityManager.registerNetworkCallback
。此外,如果阅读 JobScheduler
,但我不完全确定我是否理解正确。
我的问题是我想在 phone 连接/断开 to/from 网络时执行一些代码。当应用程序在后台时也应该发生这种情况。从 Android O 开始,如果我想 运行 我想避免的后台服务,我必须显示通知。
我尝试获取有关 phone 使用 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>
我想一定有一个简单的解决方案,但我找不到。
第二次编辑:我现在正在使用 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
- 停止第一个服务
- 结果:恭喜,您在后台有一个服务 运行ning(您第二次启动的服务)。
- onTaskRemoved 会在您打开应用程序时在第二个服务上调用,并且它会从 RAM 中清除,但不会在第一个服务终止时调用。如果您有像处理程序这样的重复操作并且不在
onTaskRemoved
中注销它,它会继续到 运行。
实际上这会启动一个前台服务,它会启动一个后台服务,然后终止。第二个服务比第一个服务长。我不确定这是否是预期的行为(也许应该提交错误报告?)但这是一种解决方法(同样,一个糟糕的方法!)。
似乎无法在连接更改时收到通知:
- 使用 Android 7.0
CONNECTIVITY_ACTION
清单中声明的接收器将不会接收广播。此外,如果接收器已在主线程上注册,则以编程方式声明的接收器仅会接收广播(因此使用服务将不起作用)。如果您仍想在后台接收更新,可以使用connectivityManager.registerNetworkCallback
- 在 Android 8.0 中存在相同的限制,但除此之外,您无法从后台启动服务,除非它是前台服务。
所有这些都允许这些解决方案:
- 启动前台服务并显示通知
- 这很可能会困扰用户
- 使用 JobService 并定期将其安排到 运行
- 根据您的设置,调用服务需要一些时间,因此连接更改后可能已经过了几秒钟。总而言之,这会延迟连接更改时应该发生的操作
这是不可能的:
connectivityManager.registerNetworkCallback(NetworkInfo, PendingIntent)
不能使用,因为如果满足条件,PendingIntent 会立即执行它的操作;它只被调用一次- 尝试以这种方式启动前台服务,该服务进入前台 1 毫秒并重新注册调用,结果类似于无限递归
因为我已经有一个 运行 处于前台模式的 VPNService(因此显示了一个通知)我实施了该服务来检查前台的连接性 运行s 如果 VPNService 没有反之亦然。这总是显示一个通知,但只有一个。
总之,我发现 8.0 更新非常不令人满意,似乎我要么必须使用不可靠的解决方案(重复 JobService),要么用永久通知打断我的用户。用户应该能够将应用程序列入白名单(仅凭存在隐藏 "XX is running in background" 通知的应用程序这一事实就足够了)。题外话就这么多
请随意扩展我的解决方案或向我展示任何错误,但这些是我的发现。
我必须对 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 接收方调用 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"/>