【问题标题】:Battery-saver + phone-call-intent => no Internet?省电+电话意图=>没有互联网?
【发布时间】:2018-02-09 02:47:46
【问题描述】:

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

现在是关于节电模式,而不是节电模式和打盹模式。它也不是关于 Service&BroadcastReceiver,而只是 BroadcastReceiver。

背景

从 Android Lollipop 开始,Google 引入了新的手动和自动方式来帮助节省电池:

“打盹”模式和“省电模式”。

在某些情况下,应用可能会因为这些技术而无法访问互联网。

问题

我正在开发一个应用程序,该应用程序需要使用在特定情况下触发的后台服务访问 Internet,如果收到重要信息,它会显示一些 UI。

作为用户,我注意到在某些情况下,它无法访问 Internet。

判断app能否上网是这样的:

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?是真的不能在manifest里注册吗?

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

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

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


编辑:似乎这根本不是服务的错,它也发生在省电模式下,没有打盹模式。

该服务的触发器是一个监听电话事件的 BroadcastReceiver,即使我在其onReceive 函数上检查 Internet 连接,我也看到它返回 false。从它启动的服务也是如此,即使它是前台服务。查看 NetworkInfo 结果,它是“BLOCKED”,它的状态确实是“DISCONNECTED”。

现在的问题是,为什么会发生这种情况。

这里有一个新的示例 POC 来检查这一点。要重现,您需要打开省电模式(使用./adb shell settings put global low_power 1 命令,或以用户身份),然后启动它,接受权限,关闭活动,然后从另一部手机呼叫此手机。您会注意到,在 Activity 上,它显示有 Internet 连接,而在 BroadcastReceiver 上,它说没有。

请注意,连接到 USB 数据线时可能会自动关闭省电模式,因此您可能需要在未连接设备时尝试一下。与启用它的用户方法相反,使用 adb 命令会阻止它。

也可以在here 找到示例项目,尽管它最初是关于打盹模式的。只需使用节电模式,即可查看问题是否发生。

电话广播接收器

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

【问题讨论】:

  • 请显示通知用户后台服务正在运行的代码。
  • @danny117 不需要。问题似乎不再与服务有关。它直接出现在 PhoneBroadcastReceiver 中,无论如何它都是启动服务的那个(如果您坚持,只需将“startService”与您创建的任何服务一起使用)。在 PhoneBroadcastReceiver 上,如果您检查 Internet 连接,您会发现它不可用。
  • 在未通知用户的情况下无法运行服务。
  • @danny117 我不明白你的意思。
  • 几个小问题,你的targetsdk是什么?此外,当您运行测试时,应用程序是在前台还是后台(根据本文档的定义 developer.android.com/about/versions/oreo/…)?最后,您是否尝试过从广播接收器启动前台服务并在onStartCommand 函数中检查连接?

标签: android internet-connection android-doze battery-saver


【解决方案1】:

所以我从issue tracker 下载了您的示例应用,按照您描述的方式对其进行了测试,并在运行 Android 6.0.1 的 Nexus 5 上找到了这些结果:


测试一的条件:

  • 应用未列入白名单
  • 使用adb shell settings put global low_power 1设置的节电模式
  • 使用adb tcpip &lt;port&gt;adb connect &lt;ip&gt;:&lt;port&gt; 无线连接的设备
  • 仅广播接收器,无服务

在本次测试中,应用的功能与您提到的一样:

后台应用 -

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 函数与系统将应用程序置于前台之间存在延迟。


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

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

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

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


编辑#1

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

MyService.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

所以这就是我推荐的。这是基于您之前在 cmets 中给出的答案 (Android check internet connection):

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

}

【讨论】:

  • 无法重现测试 3. 当测试应用程序从省电模式中被列入白名单时,无论在哪里,它都显示它有互联网连接。即使在 PhoneBroadcastReceiver 类上。也许您应该避免使用 adb-wireless 选项。只需断开连接,进行测试,然后连接即可在 LogCat 上查看结果。另外,也许尝试以用户身份启用省电模式(通过快速设置)
  • 我注意到的是,正如您所写的,经过一定的延迟后,互联网就可以使用了。前台服务似乎也很重要。否则,它会继续提示没有 Internet 连接。
  • 但这不是操作系统的错误吗?有没有一个很好的解决方法,不需要白名单?
  • 我已经对其进行了进一步测试。我尝试使用侦听连接更改的 BroadcastReceiver。它没有用。但是,在您创建服务后连接可用的延迟似乎非常低。在 HTC One M8 上不到 100 毫秒。我认为一个很好的解决方法是尝试连接互联网,例如,在服务运行后的第一秒内,如果它不可用,这意味着我们可能真的没有互联网连接,或者我们需要被列入白名单。如果您有更好的解决方法,请告诉我。
【解决方案2】:

Google 推荐的处理此问题的方法是使用 JobScheduler 或类似库(例如 Firebase JobDispatcher)在“维护窗口”之一期间有网络时安排作业。有关更多详细信息,请参阅Optimizing for Doze and App Standby。如果您确实需要在此维护窗口之外执行服务,则应将信息存储在磁盘(数据库、文件...)上并定期同步,或者在极端情况下请求将其列入白名单。

说完这些,我们来回答你的问题。

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

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

特别注意:

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

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

另外,请注意,一些手机制造商提供的节电应用程序可能会施加额外的限制并终止应用程序以节省电池电量。请参阅以下answer and discussion

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

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

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

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

关于在 manifest 中注册,如果您的应用针对 Android Oreo,是的,you must register most of the receivers programmatically

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

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

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

检查应该相同。打瞌睡模式会比 Marshmallow 更多的情况下触发,但在应用上的效果应该是完全一样的。

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

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

【讨论】:

  • JobScheduler 在这种情况下不适合应用程序的需求。该服务必须尝试“当场”访问 Internet。关于这些问题,我不明白你建议做什么才能知道没有互联网连接的原因。
  • 然后,我会走白名单路线,并尝试教育用户采取该路线的原因。关于我的建议,基本上我认为您已经尝试过的看起来不错,但我希望可能无法捕捉到一些非常边缘的情况(如 3rd 方电池保护程序)。我没有足够的经验给你更详细的解释,对不起。
  • 我想我发现省电模式也会影响后台服务访问互联网。可能吗?即使在这些州,服务也可以在不被列入白名单的情况下以某种方式访问​​互联网吗?
  • 认为是一个建议,至少在 5.0 中是这样,但可能会在以后的版本中强制执行。
  • 再次查看文档,前台应该不受影响,但建议减少网络使用(例如向服务器请求更小分辨率的图像,做更少的请求等)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-04
  • 1970-01-01
相关资源
最近更新 更多