Подтвердить что ты не робот

Battery-saver + phone-call-intent => нет Интернета?

Примечание: как оказалось, исходный вопрос был неверным в своих предположениях. Подробнее о его изменениях внизу.

Теперь об аккумуляторах, а не об аккумуляторах и режиме доза. Это также не о Service & BroadcastReceiver, а только о BroadcastReceiver.

Фон

Начиная с Android Lollipop, Google представил новые, ручные и автоматические способы экономии заряда батареи:

Режим "Дозировка" и "Аккумулятор".

В некоторых случаях приложения могут не иметь доступа к Интернету из-за этих методов.

Проблема

Я работаю над приложением, которому необходимо получить доступ к Интернету с помощью фоновой службы, которая запускается в определенных случаях, и если что-то важное получает, он показывает некоторый интерфейс.

Я заметил, как пользователь, что в некоторых случаях он не имеет доступа к Интернету.

Проверка того, может ли приложение иметь доступ к Интернету, такова:

public static boolean isInternetOn(Context context) {
    final NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
    return !(info == null || !info.isConnectedOrConnecting());
}

Проблема заключается в том, что мне нужно проверить, почему это иногда возвращает false, поэтому, если это не удается, мы должны сообщить пользователю (через уведомление, возможно), что данные не могут быть доступны, поскольку устройство ограничило приложение и предложило пользователь в белом списке приложения от оптимизации батареи.

Я не уверен, какие из них влияют на это: Doze, battery saver или both, и если это всегда так, для всех устройств, во всех случаях.

Что я пробовал

То, что я нашел, - это запрос режима Doze и Battery-saver (энергосбережение):

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

Вопросы

Короче говоря, я просто хочу узнать, не является ли причиной невозможности доступа к Интернету, потому что нет подключения к Интернету или если приложение только что ограничено в настоящее время из-за определенных оптимизаций батареи.

Только в случае ограничения, я мог бы предупредить пользователя, что если он будет в порядке с ним, приложение будет белым, чтобы он мог работать по-прежнему.

Вот мои вопросы по этому поводу:

  • Какое из вышеперечисленных предупреждений позволяет использовать фоновые службы приложений для доступа в Интернет? Все ли они вызывают это? Является ли он специфичным для устройства? "Интерактивный" влияет на него?

  • Что означает "сила-простоя", если есть уже способ перехода в "легкие" и "глубокие" состояния доз? Есть ли способ вернуться в нормальный режим до reset? Я попробовал несколько команд, но только перезапуск устройства действительно получил его до reset обратно к нормальному...

  • Создает ли созданный BroadcastReceiver, чтобы проверить его правильно? Будет ли во всех случаях инициировать отказ в доступе к Интернету из-за всех особых случаев? Верно ли, что я не могу зарегистрироваться в манифесте?

  • Можно ли проверить, не является ли причиной невозможности доступа к Интернету, потому что нет подключения к Интернету или если приложение только что ограничено из-за определенных оптимизаций батареи?

  • Ограничены ли ограничения подключения к Интернету для фоновых служб в особых случаях на Android O? Может быть, еще больше случаев я должен проверить?

  • Предположим, что я изменяю службу для запуска на переднем плане (с уведомлением), будет ли это охватывать все случаи и всегда иметь доступ к Интернету, независимо от того, в каком специальном состоянии находится устройство?


EDIT: кажется, что это не служебная ошибка вообще, и что это происходит и в режиме экономии заряда батареи, без режима Doze.

Триггер службы - это BroadcastReceiver, который слушает события телефонных звонков, и даже если я проверяю подключение к Интернету в его функции onReceive, я вижу, что он возвращает false. То же самое касается службы, которая запускается с нее, даже если она используется для переднего плана. Посмотрев на результат NetworkInfo, он "BLOCKED", и его состояние действительно "ОТКЛЮЧЕНО".

Вопрос, почему это происходит.

Здесь новый образец POC, чтобы проверить это. Чтобы воспроизвести, вам необходимо включить режим экономии заряда батареи (используя команду ./adb shell settings put global low_power 1 или как пользователь), затем запустите ее, примите разрешения, закрыть активность и позвонить с другого телефона на этот. Вы заметите, что в действии он показывает, что есть подключение к Интернету, и на BroadcastReceiver он говорит, что это не так.

Обратите внимание, что при подключении к USB-кабелю автоматически отключается режим экономии заряда батареи, поэтому вам может потребоваться попробовать, когда устройство не подключено. Использование команды adb предотвращает ее, в отличие от пользовательского метода ее включения.

Образец проекта также можно найти здесь, хотя первоначально он предназначался для режима 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);
        }
    }
}
4b9b3361

Ответ 1

Итак, я загрузил ваше примерное приложение из трекер ошибок, протестировал его так, как вы описали, и нашел эти результаты на Nexus 5 Android 6.0.1:


Условия для теста 1:

  • Приложение не включено whitelisted
  • Режим экономии заряда батареи установлен с помощью 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 и системой, помещающей приложение на передний план.


Я даже выполнил тесты 2 и 3 с помощью adb shell dumpsys deviceidle force-idle и получил аналогичные результаты для теста 3, где первый журнал не был подключен, но все последующие журналы показывали интернет-соединение.

Я считаю, что все функции по назначению, так как аккумуляторная батарея на устройстве заявляет:

Чтобы увеличить время автономной работы аккумулятора, экономия заряда аккумулятора снижает производительность устройства и ограничивает возможности использования вибрации, определения местоположения и большинства фоновых данных. Электронная почта, обмен сообщениями и другие приложения, которые полагаются на синхронизацию, могут не обновляться, если вы не откройте их.

Итак, если вы в настоящее время не используете приложение или не используете приложение и, работающий переднего плана, вы можете ожидать, что интернет-соединение не будет доступно для вашего приложения, если оно находится в батарее заставки или Doze.


Изменить # 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 проверить подключение к Интернету):

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

}

Ответ 2

Рекомендуемым Google способом для этого является использование JobScheduler или аналогичная библиотека (например, Firebase JobDispatcher), чтобы запланировать задание при наличии сети во время одного из "окон обслуживания". Для получения дополнительной информации см. Оптимизация для Doze и App Standby. Если вам действительно нужно выполнить обслуживание из этого окна обслуживания, вы должны хранить информацию на диске (DB, файлы...) и периодически синхронизировать ее, либо в крайнем случае запрос должен быть включен в белый список.

С учетом этого отпустите свои вопросы.

Какое из вышеперечисленных действий предотвращает использование фоновых служб приложениями для доступа в Интернет? Все ли они вызывают это? Является ли он специфичным для устройства? "Интерактив" влияет на него?

Дожимный режим. В режиме экономии батареи в нем указано, что "Ограничивает... большинство фоновых данных", но я думаю, что это всего лишь рекомендация для приложений. См. здесь.

В частности, обратите внимание:

RESTRICT_BACKGROUND_STATUS_ENABLED Пользователь включил Data Saver для этого приложения. Приложения должны стараться ограничить использование данных на переднем плане и грамотно обрабатывать ограничения на использование фоновых данных.

Итак, это похоже на совет, а не на то, что он соблюдает.

Также обратите внимание, что у некоторых производителей телефонов есть приложения для экономии заряда батареи, которые могут налагать дополнительные ограничения и убивать приложения, чтобы сэкономить аккумулятор. См. Следующий ответ и обсуждение.

Что означает "сила-простоя", если уже есть способ перейти к "легким" и "глубоким" состояниям доз? Есть ли способ вернуться в нормальный режим до reset? Я попробовал несколько команд, но только перезапуск устройства действительно получил его до reset обратно к нормальному...

У меня нет дополнительного опыта для совместного использования, кроме документации, которую вы, вероятно, уже прочитали в разделах "Тестирование" this и эти статьи.

Создает ли BroadcastReceiver, чтобы проверить его правильно? Будет ли во всех случаях инициировать отказ в доступе к Интернету из-за всех особых случаев? Верно ли, что я не могу зарегистрироваться в манифесте?

Не совсем уверен, сможете ли вы проверить все случаи, но вместо этого следует попытаться следовать стратегии "синхронизируйте, когда сеть доступна", если это возможно.

О регистрации в манифесте, если ваше приложение предназначено для Android Oreo, да, вы должны зарегистрировать большинство приемников программно.

Можно ли проверить, не является ли причиной невозможности доступа к Интернету, действительно ли из-за отсутствия подключения к Интернету или если приложение только что ограничено из-за определенных оптимизаций батареи?

Код, который вы поделили, выглядит неплохо, но я не ожидаю, что на 100% уверен, так как иногда одновременно могут возникать несколько условий.

Ограничены ли ограничения подключения к Интернету для фоновых служб в особых случаях на Android O? Может быть, еще больше случаев я должен проверить?

Проверки должны быть одинаковыми. Режим Doz будет срабатывать в большем количестве случаев, чем в Marshmallow, но эффекты в приложении должны быть точно такими же.

Предположим, что я изменяю службу для запуска на переднем плане (с уведомлением), будет ли это охватывать все случаи и всегда иметь доступ к Интернету, независимо от того, какое специальное состояние находится в устройстве?

Как я уже говорил, в некоторых устройствах (приложениях с батареями) приложение будет убито, поэтому оно, вероятно, не сработает. На складе Android, вероятно, поднимут некоторые ограничения, но я не могу подтвердить, потому что я не тестировал себя.