web-dev-qa-db-ja.com

バッテリーセーバー+電話の意図=>インターネットがありませんか?

注:結局のところ、元の質問はその仮定が正しくありませんでした。下部にある編集の詳細を参照してください。

今では、バッテリーセーバー&ドーズモードではなく、バッテリーセーバーについてです。また、Service&BroadcastReceiverではなく、BroadcastReceiverだけを対象としています。

バックグラウンド

Android Lollipop)から、Googleはバッテリー節約に役立つ新しい手動および自動の方法を導入しました。

「居眠り」モード、「バッテリーセーバー」。

場合によっては、これらの手法が原因でアプリがインターネットにアクセスできないことがあります。

問題

特定のケースでトリガーされるバックグラウンドサービスを使用してインターネットにアクセスする必要があるアプリで作業しています。重要なものを受信すると、UIが表示されます。

ユーザーとして、インターネットにアクセスできない場合があることに気づきました。

アプリがインターネットにアクセスできるかどうかのチェックは次のとおりです。

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

問題は、これが時々falseを返す理由を確認する必要があることです。失敗した場合は、デバイスがアプリを制限しているためにデータにアクセスできないことをユーザーに(おそらく通知を介して)通知し、ユーザーにバッテリー最適化からアプリをホワイトリストに登録します。

Doze、バッテリーセーバー、またはその両方のどれがこれに影響するのかわかりません。常にこのようになっている場合は、すべてのデバイスで、すべての場合に。

私が試したこと

私が見つけたのは、Dozeモードとバッテリーセーバー(省電力)モードのクエリ方法です。

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で正しく確認できますか?すべての特別な場合のためにインターネットへのアクセスが拒否されるすべての場合にトリガーされますか?マニフェストで登録できないのは本当ですか?

  4. インターネットにアクセスできない理由が実際にインターネットに接続されていないためか、特定のバッテリーの最適化のためにアプリが現在制限されているかどうかを確認することはできますか?

  5. 特別な場合のバックグラウンドサービスのインターネット接続の制限はAndroid O?もっと多くの場合を確認する必要がありますか?

  6. (通知付きで)フォアグラウンドで実行するようにサービスを変更するとします。これはすべてのケースをカバーし、デバイスがどのような特別な状態にあるかに関係なく、常にインターネットにアクセスできますか?


編集:それはサービスのせいではないようで、Dozeモードなしのバッテリーセーバーモードでも発生するようです。

このサービスのトリガーは、通話イベントをリッスンするBroadcastReceiverであり、そのonReceive関数でインターネット接続を確認しても、falseが返されることがわかります。フォアグラウンドサービスであっても、そこから開始されるサービスについても同じことが言えます。 NetworkInfoの結果を見ると、「BLOCKED」であり、その状態は実際には「DISCONNECTED」です。

今の質問は、なぜこれが起こるのかということです。

これを確認するための新しいサンプルPOCを次に示します。再現するには、バッテリーセーバーモードをオンにして(./adb Shell settings put global low_power 1コマンドを使用するか、ユーザーとして)、起動して権限を受け入れ、アクティビティを閉じて、別の電話からこの電話に電話をかける必要があります。アクティビティではインターネット接続があることを示し、BroadcastReceiverではインターネット接続がないことを示しています。

USBケーブルに接続するとバッテリーセーバーモードが自動的にオフになる場合があるため、デバイスが接続されていないときに試してみる必要がある場合があります。 adbコマンドを使用すると、ユーザーが有効にする方法とは対照的に、それを防ぐことができます。

サンプルプロジェクトは、もともとDozeモードに関するものでしたが、 here にもあります。代わりにバッテリーセーバーモードを使用して、問題が発生することを確認してください。

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

そこで、サンプルアプリを 課題追跡システム からダウンロードし、説明した方法でテストしたところ、Android 6.0.1 :)を実行しているNexus5でこれらの結果が見つかりました。


テスト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

このテストは、最初のログではインターネット接続が得られなかったという点で興味深いものでしたが、2番目のログでは、最初のログから約4秒後、フォアグラウンドサービスが確立されてからかなり後になりました。 2回目にテストを実行したとき、両方のログが真でした。これは、呼び出されているstartForeground関数とシステムがアプリをフォアグラウンドに配置するまでの遅延を示しているようです。


adb Shell dumpsys deviceidle force-idleを使用してテスト2と3を実行したところ、最初のログが接続されていなかったテスト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
Pablo Baxter

これを処理するGoogle推奨の方法は、 JobScheduler または同様のライブラリ(例: Firebase JobDispatcher )を使用して、「メンテナンスウィンドウ」の1つにネットワークがあるときにジョブをスケジュールすることです。詳細については、 Dozeおよびアプリスタンバイの最適化 を参照してください。このメンテナンスウィンドウの外でサービスを実行する必要がある場合は、情報をディスク(DB、ファイルなど)に保存して定期的に同期するか、極端な場合はホワイトリストに登録する必要があります。

それが邪魔にならないように、あなたの質問に行きましょう。

上記のうち、アプリのバックグラウンドサービスがインターネットにアクセスできないようにするのはどれですか?それらのすべてがそれを引き起こしますか?デバイス固有ですか? 「インタラクティブ」はそれに影響しますか?

確かに居眠りモード。バッテリー節約モードでは、「制限...ほとんどのバックグラウンドデータ」と表示されますが、これはアプリケーションの推奨事項にすぎないと思います。 ここ を参照してください。

具体的には、次の点に注意してください。

RESTRICT_BACKGROUND_STATUS_ENABLEDユーザーがこのアプリのデータセーバーを有効にしました。アプリは、フォアグラウンドでのデータ使用を制限し、バックグラウンドデータ使用の制限を適切に処理するように努める必要があります。

したがって、それはアドバイスのように見えますが、強制されるものではありません。

また、一部の電話メーカーには、追加の制限を課したり、バッテリーを節約するためにアプリケーションを強制終了したりする可能性のあるバッテリー節約アプリケーションがあることに注意してください。次を参照してください 回答とディスカッション

「軽い」および「深い」居眠り状態に移行する方法がすでにある場合、「フォースアイドル」とは何ですか?居眠りモードを通常に戻す方法もありますか?複数のコマンドを試しましたが、デバイスを再起動するだけで、実際には通常の状態にリセットされました...

this および this の記事の「テスト」セクションですでに読んだドキュメントを除いて、共有する追加の経験はありません。

作成したBroadcastReceiverで正しく確認できますか?すべての特別な場合のためにインターネットへのアクセスが拒否されるすべての場合にトリガーされますか?マニフェストで登録できないのは本当ですか?

すべてのケースを確認できるかどうかは完全にはわかりませんが、可能であれば「ネットワークが利用可能なときに同期する」戦略に従うようにしてください。

マニフェストへの登録について、アプリがターゲットにしている場合Android Oreo、はい、 ほとんどのレシーバーをプログラムで登録する必要があります

インターネットにアクセスできない理由が実際にインターネットに接続されていないためか、特定のバッテリーの最適化のためにアプリが現在制限されているかどうかを確認することはできますか?

共有したコードは良さそうですが、複数の条件が同時に発生することがあるため、100%確実であるとは思いません。

特別な場合のバックグラウンドサービスのインターネット接続の制限はAndroid O?もっと多くの場合を確認する必要がありますか?

チェックは同じである必要があります。居眠りモードはマシュマロよりも多くの場合にトリガーされますが、アプリへの影響はまったく同じである必要があります。

(通知付きで)フォアグラウンドで実行するようにサービスを変更するとします。これはすべてのケースをカバーし、デバイスがどのような特別な状態にあるかに関係なく、常にインターネットにアクセスできますか?

前にも言ったように、一部のデバイス(バッテリーセーバーアプリ)ではアプリが強制終了されるため、おそらく機能しません。在庫のAndroidでは、おそらくいくつかの制限がありますが、自分でテストしていないため、確認できません。