28

注意:事实证明,原始问题的假设不正确。在底部查看有关其编辑的更多详细信息。

现在是关于节电模式,而不是节电模式和打盹模式。它也不是关于 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

问题

简而言之,我只想知道无法访问互联网的原因是否确实是因为没有互联网连接,或者应用程序当前是否由于某些电池优化而受到限制。

仅在受到限制的情况下,我可以警告用户,如果他没问题,该应用程序将被列入白名单,以便它仍然可以正常工作。

以下是我对此的疑问:

  1. 以上哪项阻止了应用程序的后台服务访问互联网?都是造成的吗?它是特定于设备的吗?“互动”会影响它吗?

  2. 如果已经有办法进入“轻度”和“深度”打瞌睡状态,那么“强制空闲”是什么?还有没有办法将打盹模式重置为正常?我尝试了多个命令,但只有重新启动设备才真正让它恢复正常......

  3. 我创建的 BroadcastReceiver 是否允许正确检查?是否会在所有情况下触发由于所有特殊情况而拒绝访问 Internet?我不能在清单中注册它是真的吗?

  4. 是否可以检查无法访问互联网的原因是否确实是因为没有互联网连接,或者应用程序当前是否由于某些电池优化而受到限制?

  5. Android O 上特殊情况下后台服务的互联网连接限制有变化吗?也许我应该检查更多的情况?

  6. 假设我将服务更改为在前台运行(带有通知),这是否涵盖所有情况,并且始终可以访问 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);
        }
    }
}
4

2 回答 2

4

因此,我从问题跟踪器下载了您的示例应用程序,按照您描述的方式对其进行了测试,并在运行 Android 6.0.1 的 Nexus 5 上找到了这些结果:


测试1的条件:

  • 应用未列入白名单
  • 省电模式设置使用adb shell settings put global low_power 1
  • 通过无线连接的设备使用adb tcpip <port>adb connect <ip>:<port>
  • 只有广播接收器,没有服务

在此测试中,应用程序的功能与您提到的一样:

后台应用程序 -

D/AppLog: PhoneBroadcastReceiver:isInternetOn:false
D/AppLog: PhoneBroadcastReceiver:isInternetOn:false

测试2的条件:

  • 与测试 1 相同,但更改如下

  • BroadcastReceiver 启动服务(示例如下)

    public class PhoneService extends Service {
    
        public void onCreate() {
            super.onCreate();
            startForeground(1, new Notification.Builder(this)
                    .setSmallIcon(R.mipmap.ic_launcher_foreground)
                    .setContentTitle("Test title")
                    .setContentText("Test text")
                    .getNotification());
        }
    
        public int onStartCommand(Intent intent, int flags, int startId) {
            final String msg = "PhoneService:isInternetOn:" + isInternetOn(this);
            Log.d("AppLog", msg);
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
            return START_STICKY;
        }
    
        public static boolean isInternetOn(Context context) {
            final NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
            return !(info == null || !info.isConnectedOrConnecting());
        }
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
    

这个测试给了我和上面一样的结果。


测试 3 的条件:

  • 与测试 2 相同,但更改如下
  • 应用被列入电池优化白名单

后台应用程序 -

D/AppLog: PhoneService:isInternetOn:false
D/AppLog: PhoneService:isInternetOn:true

这个测试很有趣,第一个日志没有给我互联网连接,但第二个日志给了我,这比第一个日志晚了大约 4 秒,并且在它建立前台服务之后很长时间。当我第二次运行测试时,两个日志都是正确的。这似乎表明startForeground被调用的函数与系统将应用程序置于前台之间存在延迟。


我什至使用 运行了测试 2 和 3 adb shell dumpsys deviceidle force-idle,得到了与测试 3 相似的结果,其中第一个日志没有连接,但所有后续日志都显示互联网连接。

我相信这一切功能都符合预期,因为设备上的电池保护程序状态:

为了帮助延长电池寿命,省电模式会降低设备的性能并限制振动、定位服务和大多数后台数据。电子邮件、消息和其他依赖同步的应用程序可能不会更新,除非您打开它们。

因此,除非您当前正在使用该应用程序,或者您将该应用程序列入白名单运行前台服务,否则如果您的应用程序处于节电模式或打盹模式,您可以预期没有可用的互联网连接。


编辑#1

与使用某些计时器重新检查互联网连接相比,这可能是一种不同的解决方法:

我的服务.java

@Override
public void onCreate() {
    super.onCreate();
    Log.d("AppLog", "MyService:onCreate isInternetOn:" + PhoneBroadcastReceiver.isInternetOn(this));
    if (!PhoneBroadcastReceiver.isInternetOn(this)) {
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            final ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(CONNECTIVITY_SERVICE);
            connectivityManager.registerNetworkCallback(new NetworkRequest.Builder()
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                    .build(), new ConnectivityManager.NetworkCallback() {
                @Override
                public void onAvailable(Network network) {
                    //Use this network object to perform network operations
                    connectivityManager.unregisterNetworkCallback(this);
                }
            });
        }
    }
}

如果有可以使用的网络连接,这将立即返回,或者等到有连接。如果您在一段时间后没有收到任何结果,您可能可以从这里使用Handler取消注册,尽管我可能只是让它保持活动状态。


编辑#2

所以这就是我推荐的。这是基于您之前在评论中给出的答案(Android 检查互联网连接):

@Override
public void onCreate() {
    super.onCreate();
    Log.d("AppLog", "MyService:onCreate isInternetOn:" + PhoneBroadcastReceiver.isInternetOn(this));
    if (!PhoneBroadcastReceiver.isInternetOn(this)) {
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            final ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(CONNECTIVITY_SERVICE);
            connectivityManager.registerNetworkCallback(new NetworkRequest.Builder()
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                    .build(), new ConnectivityManager.NetworkCallback() {
                @Override
                public void onAvailable(Network network) {
                    isConnected(network); //Probably add this to a log output to verify this actually works for you
                    connectivityManager.unregisterNetworkCallback(this);
                }
            });
        }
    }
}

public static boolean isConnected(Network network) {
    
    if (network != null) {
        try {
            URL url = new URL("http://www.google.com/");
            HttpURLConnection urlc = (HttpURLConnection)network.openConnection(url);
            urlc.setRequestProperty("User-Agent", "test");
            urlc.setRequestProperty("Connection", "close");
            urlc.setConnectTimeout(1000); // mTimeout is in seconds
            urlc.connect();
            if (urlc.getResponseCode() == 200) {
                return true;
            } else {
                return false;
            }
        } catch (IOException e) {
            Log.i("warning", "Error checking internet connection", e);
            return false;
        }
    }

    return false;

}
于 2017-09-27T20:24:42.763 回答
0

谷歌推荐的处理方法是使用JobScheduler或类似的库(例如Firebase JobDispatcher),在“维护窗口”之一期间有网络时安排作业。有关更多详细信息,请参阅针对打盹和应用待机进行优化。如果您确实需要在此维护窗口之外执行您的服务,您应该将信息存储在磁盘(数据库、文件...)上并定期同步它,或者在极端情况下请求将其列入白名单。

有了这个,让我们来回答你的问题。

以上哪项阻止了应用程序的后台服务访问互联网?都是造成的吗?它是特定于设备的吗?“互动”会影响它吗?

打瞌睡模式是肯定的。省电模式,它声明“限制......大多数背景数据”,但我认为只是对应用程序的建议。见这里

具体来说,请注意:

RESTRICT_BACKGROUND_STATUS_ENABLED 用户已为此应用启用流量节省程序。应用程序应该努力限制前台的数据使用,并优雅地处理对后台数据使用的限制。

所以,它看起来像是一个建议,而不是强制执行的东西。

此外,请注意,一些手机制造商提供的节电应用程序可能会施加额外的限制并终止应用程序以节省电池电量。请参阅以下答案和讨论

如果已经有办法进入“轻度”和“深度”打瞌睡状态,那么“强制空闲”是什么?还有没有办法将打盹模式重置为正常?我尝试了多个命令,但只有重新启动设备才真正让它恢复正常......

除了您可能已经在本文本文的“测试”部分阅读的文档之外,我没有其他经验可以分享。

我创建的 BroadcastReceiver 是否允许正确检查?是否会在所有情况下触发由于所有特殊情况而拒绝访问 Internet?我不能在清单中注册它是真的吗?

不完全确定您是否能够检查所有情况,但如果可能,应尝试遵循“网络可用时同步”策略。

关于在 manifest 中注册,如果您的应用程序针对 Android Oreo,是的,您必须以编程方式注册大多数接收器

是否可以检查无法访问互联网的原因是否确实是因为没有互联网连接,或者应用程序当前是否由于某些电池优化而受到限制?

您共享的代码看起来不错,但我不希望 100% 确定,因为有时可能同时发生多个条件。

Android O 上特殊情况下后台服务的互联网连接限制有变化吗?也许我应该检查更多的情况?

检查应该是一样的。打瞌睡模式会在比棉花糖更多的情况下触发,但对应用程序的影响应该是完全一样的。

假设我将服务更改为在前台运行(带有通知),这是否涵盖所有情况,并且始终可以访问 Internet,无论设备处于什么特殊状态?

正如我之前所说,在某些设备(省电应用程序)中,该应用程序将被终止,因此它可能无法正常工作。现货Android,可能会升一些限制,但我无法确认,因为我自己没有测试过。

于 2017-08-31T09:08:59.547 回答