电池保护程序+电话呼叫意图=>没有互联网?

时间:2017-08-31 08:12:50

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

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

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

背景

从Android Lollipop开始,Google推出了新的,手动和自动方式来帮助节省电量:

"打盹"模式和"省电"。

在某些情况下,由于这些技术,应用可能无法访问互联网。

问题

我处理的应用程序需要使用在特定情况下触发的后台服务访问Internet,如果收到重要内容,则会显示一些UI。

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

检查应用是否可以访问互联网是这样的:

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. 以上哪项会阻止应用访问互联网的后台服务?他们所有人都这样做吗?它是特定于设备的吗?是&#34;互动&#34;影响吗?

  2. 什么&#34;强制空闲&#34;因为,如果已经有办法去&#34;光&#34;和#34;深&#34;打瞌睡的状态?还有办法将打盹模式恢复正常吗?我尝试了多个命令,但只是重新启动设备才真正让它恢复正常......

  3. 我创建的BroadcastReceiver是否允许正确检查?在所有情况下都会触发由于所有特殊情况而拒绝访问互联网吗?我是否可以在舱单中注册?

  4. 是否可以检查无法访问互联网的原因,是因为没有互联网连接,还是因为某些电池优化导致应用目前受到限制?

  5. 在Android O上是否更改了特殊情况下后台服务的Internet连接限制?也许更多的情况我应该检查一下?

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

  7. 编辑:它似乎根本不是服务的故障,而且它也是在电池保护模式下发生的,没有打盹模式。

    该服务的触发器是一个侦听电话呼叫事件的BroadcastReceiver,即使我检查其onReceive函数的Internet连接,我也看到它返回false。即使它是前台服务,从它启动的服务也是如此。查看NetworkInfo结果,它已经&#34; BLOCKED&#34;,其状态确实是&#34; DISCONNECTED&#34;。

    现在问题,这就是为什么会这样。

    这是一个新的样本POC来检查这个。要重现,您需要打开省电模式(使用./adb shell settings put global low_power 1命令或用户),然后启动它,接受权限,关闭活动,并从另一部手机拨打此电话。您会注意到,在活动中,它显示有Internet连接,而在BroadcastReceiver上,它表示它没有。

    请注意,连接USB电缆时,节电模式可能会自动关闭,因此您可能需要在未连接设备时进行尝试。使用adb命令可以阻止它,而不是启用它的用户方法。

    示例项目也可以找到here,即使它原本是关于Doze模式的。只需使用节电模式,即可发现问题。

    PhoneBroadcastReceiver

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

2 个答案:

答案 0 :(得分:2)

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

测试1的条件:

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

在此测试中,应用程序正如您所述:

应用程序在后台 -

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

所以这就是我推荐的内容。这是基于您在之前的评论(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;

}

答案 1 :(得分:0)

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

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

  

以上哪项会阻止应用访问互联网的后台服务?他们所有人都这样做吗?它是特定于设备的吗? “互动”会影响它吗?

打盹模式肯定。电池省电模式,它声明“限制...大多数背景数据”,但我认为这只是一个应用程序的建议。请参阅here

具体来说,请注意:

  

RESTRICT_BACKGROUND_STATUS_ENABLED   用户已为此应用启用了Data Saver。应用应努力限制前台的数据使用,并优雅地处理对后台数据使用的限制。

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

此外,请注意,某些手机制造商的电池节电应用程序可能会施加额外的限制并杀死应用程序以节省电池电量。请参阅以下answer and discussion

  

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

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

  

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

不完全确定您是否能够检查所有案例,但如果可能的话,应尝试遵循“在网络可用时进行同步”策略。

关于在清单中注册,如果您的应用定位到Android Oreo,是的,you must register most of the receivers programmatically

  

是否可以检查无法访问互联网的原因,是因为没有互联网连接,还是由于某些电池优化导致应用目前受到限制?

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

  

在Android O上是否更改了针对特殊情况的后台服务的Internet连接限制?也许更多的情况我应该检查一下?

支票应该是一样的。在比棉花糖更多的情况下会触发打盹模式,但对应用程序的影响应该完全相同。

  

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

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