注意:事实证明,原始问题的假设不正确。在底部查看有关其编辑的更多详细信息。
现在是关于节电模式,而不是节电模式和打盹模式。它也不是关于 Service&BroadcastReceiver,而只是 BroadcastReceiver。
背景
从 Android Lollipop 开始,Google 引入了新的手动和自动方式来帮助节省电池:
“打盹”模式和“省电模式”。
在某些情况下,由于这些技术,应用程序可能无法访问 Internet。
问题
我正在开发一个需要使用在特定情况下触发的后台服务访问 Internet 的应用程序,如果收到重要的信息,它会显示一些 UI。
作为用户,我注意到在某些情况下,它无法访问 Internet。
应用程序是否可以访问互联网的检查是这样的:
public static boolean isInternetOn(Context context) {
final NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
return !(info == null || !info.isConnectedOrConnecting());
}
问题是我需要检查为什么有时会返回 false,因此如果失败,我们应该告诉用户(可能通过通知)数据无法访问,因为设备限制了应用程序,并为用户提供从电池优化中将应用列入白名单。
我不确定哪些会影响这一点:打瞌睡、省电模式,或两者兼而有之,如果总是这样,对于所有设备,在所有情况下。
我试过的
我找到的是如何查询打盹模式和省电(省电)模式:
public class PowerSaverHelper {
public enum PowerSaveState {
ON, OFF, ERROR_GETTING_STATE, IRRELEVANT_OLD_ANDROID_API
}
public enum WhiteListedInBatteryOptimizations {
WHITE_LISTED, NOT_WHITE_LISTED, ERROR_GETTING_STATE, IRRELEVANT_OLD_ANDROID_API
}
public enum DozeState {
NORMAL_INTERACTIVE, DOZE_TURNED_ON_IDLE, NORMAL_NON_INTERACTIVE, ERROR_GETTING_STATE, IRRELEVANT_OLD_ANDROID_API
}
@NonNull
public static DozeState getDozeState(@NonNull Context context) {
if (VERSION.SDK_INT < VERSION_CODES.M)
return DozeState.IRRELEVANT_OLD_ANDROID_API;
final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm == null)
return DozeState.ERROR_GETTING_STATE;
return pm.isDeviceIdleMode() ? DozeState.DOZE_TURNED_ON_IDLE : pm.isInteractive() ? DozeState.NORMAL_INTERACTIVE : DozeState.NORMAL_NON_INTERACTIVE;
}
@NonNull
public static PowerSaveState getPowerSaveState(@NonNull Context context) {
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)
return PowerSaveState.IRRELEVANT_OLD_ANDROID_API;
final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm == null)
return PowerSaveState.ERROR_GETTING_STATE;
return pm.isPowerSaveMode() ? PowerSaveState.ON : PowerSaveState.OFF;
}
@NonNull
public static WhiteListedInBatteryOptimizations getIfAppIsWhiteListedFromBatteryOptimizations(@NonNull Context context, @NonNull String packageName) {
if (VERSION.SDK_INT < VERSION_CODES.M)
return WhiteListedInBatteryOptimizations.IRRELEVANT_OLD_ANDROID_API;
final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm == null)
return WhiteListedInBatteryOptimizations.ERROR_GETTING_STATE;
return pm.isIgnoringBatteryOptimizations(packageName) ? WhiteListedInBatteryOptimizations.WHITE_LISTED : WhiteListedInBatteryOptimizations.NOT_WHITE_LISTED;
}
//@TargetApi(VERSION_CODES.M)
@SuppressLint("BatteryLife")
@RequiresPermission(permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
@Nullable
public static Intent prepareIntentForWhiteListingOfBatteryOptimization(@NonNull Context context, @NonNull String packageName, boolean alsoWhenWhiteListed) {
if (VERSION.SDK_INT < VERSION_CODES.M)
return null;
if (ContextCompat.checkSelfPermission(context, permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED)
return null;
final WhiteListedInBatteryOptimizations appIsWhiteListedFromPowerSave = getIfAppIsWhiteListedFromBatteryOptimizations(context, packageName);
Intent intent = null;
switch (appIsWhiteListedFromPowerSave) {
case WHITE_LISTED:
if (alsoWhenWhiteListed)
intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
break;
case NOT_WHITE_LISTED:
intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).setData(Uri.parse("package:" + packageName));
break;
case ERROR_GETTING_STATE:
case IRRELEVANT_OLD_ANDROID_API:
default:
break;
}
return intent;
}
/**
* registers a receiver to listen to power-save events. returns true iff succeeded to register the broadcastReceiver.
*/
@TargetApi(VERSION_CODES.M)
public static boolean registerPowerSaveReceiver(@NonNull Context context, @NonNull BroadcastReceiver receiver) {
if (VERSION.SDK_INT < VERSION_CODES.M)
return false;
IntentFilter filter = new IntentFilter();
filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
context.registerReceiver(receiver, filter);
return true;
}
}
我想我还找到了一种在连接到设备时检查它们的方法:
省电:
./adb shell settings put global low_power [1|0]
打瞌睡状态:
./adb shell dumpsys deviceidle step [light|deep]
和 :
./adb shell dumpsys deviceidle force-idle
问题
简而言之,我只想知道无法访问互联网的原因是否确实是因为没有互联网连接,或者应用程序当前是否由于某些电池优化而受到限制。
仅在受到限制的情况下,我可以警告用户,如果他没问题,该应用程序将被列入白名单,以便它仍然可以正常工作。
以下是我对此的疑问:
以上哪项阻止了应用程序的后台服务访问互联网?都是造成的吗?它是特定于设备的吗?“互动”会影响它吗?
如果已经有办法进入“轻度”和“深度”打瞌睡状态,那么“强制空闲”是什么?还有没有办法将打盹模式重置为正常?我尝试了多个命令,但只有重新启动设备才真正让它恢复正常......
我创建的 BroadcastReceiver 是否允许正确检查?是否会在所有情况下触发由于所有特殊情况而拒绝访问 Internet?我不能在清单中注册它是真的吗?
是否可以检查无法访问互联网的原因是否确实是因为没有互联网连接,或者应用程序当前是否由于某些电池优化而受到限制?
Android O 上特殊情况下后台服务的互联网连接限制有变化吗?也许我应该检查更多的情况?
假设我将服务更改为在前台运行(带有通知),这是否涵盖所有情况,并且始终可以访问 Internet,无论设备处于什么特殊状态?
编辑:似乎这根本不是服务的错,而且它也发生在省电模式下,没有打盹模式。
该服务的触发器是一个侦听电话事件的 BroadcastReceiver,即使我在其onReceive
函数上检查 Internet 连接,我也看到它返回 false。从它启动的服务也是如此,即使它是前台服务。查看 NetworkInfo 结果,它是“BLOCKED”,它的状态确实是“DISCONNECTED”。
现在的问题是,为什么会发生这种情况。
这是一个新的示例 POC 来检查这一点。要重现,您需要打开省电模式(使用./adb shell settings put global low_power 1
命令或以用户身份),然后启动它,接受权限,关闭活动,然后从另一部手机呼叫此手机。您会注意到,在活动上,它显示有 Internet 连接,而在 BroadcastReceiver 上,它说没有。
请注意,连接到 USB 数据线时可能会自动关闭省电模式,因此您可能需要在未连接设备时尝试一下。与启用它的用户方法相反,使用 adb 命令可以防止它。
示例项目也可以在这里找到,尽管它最初是关于打盹模式的。只需使用节电模式,即可查看问题是否发生。
电话广播接收器
public class PhoneBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
Log.d("AppLog", "PhoneBroadcastReceiver:isInternetOn:" + isInternetOn(context));
}
public static boolean isInternetOn(Context context) {
final NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
return !(info == null || !info.isConnectedOrConnecting());
}
}
显现
<manifest package="com.example.user.myapplication" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name=".PhoneBroadcastReceiver">
<intent-filter >
<action android:name="android.intent.action.PHONE_STATE"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
</intent-filter>
</receiver>
</application>
</manifest>
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("AppLog", "MainActivity: isInternetOn:" + PhoneBroadcastReceiver.isInternetOn(this));
if (VERSION.SDK_INT >= VERSION_CODES.M) {
requestPermissions(new String[]{permission.READ_PHONE_STATE, permission.PROCESS_OUTGOING_CALLS}, 1);
}
}
}