LoginSignup
16
17

More than 1 year has passed since last update.

AndroidのDozeを回避した話

Last updated at Posted at 2020-03-20

※ 2021/07/23に施策6を追加
※ 2021/01/02にAndroid10以上でのdozeの回避方法がわかったのでタイトルも含めて更新しています

はじめに

DozeはAndroid6から導入された素晴らしいバッテリー消費抑制機能です。

内容としては、バッテリー駆動かつ、静止状態かつ、画面OFFの状態が一定時間続くとで端末をスヤァーっとお眠りさせてバックグラウンド処理をさせなくするものです。

無法地帯と化していたバックグラウンドでのアプリの処理に制限をかけた事によりバッテリーの消費量は大幅に減ったと思います。

一般ユーザにはありがたい機能ですね。

しかし、企業向けアプリにとってはちょっと厄介な機能だと思っています。

顧客からバックグラウンドで位置情報やセンサーデータを取り続けたいんだーと言われたらやるしかありません。

そこで今回は、色々な方法でDozeを回避できるか試してしいこうと思います。

施策

駄目だった施策も交えつつ一つずつ試した結果を説明します。

施策1:フォアグラウンドサービスで処理を行う

結論から言いますが、駄目です。

制限されるのはバックグラウンド処理だけだからフォアグラウンドとして動作するフォアグラウンドサービスは大丈夫じゃないか?と思ったら大間違いです。きっちりDozeの対象です。

フォアグラウンドサービスにすることによってOSからプロセスをキルされることはほぼなくなりますが、それだけです。

施策2:ホワイトリストに入れてDozeの条件から除外する

結論から言いますが、駄目です。

設定→電池→メニュー開く→電池の最適化で表示されるホワイトリストにアプリを追加するとDozeの影響をまったく受けないと思われるかもしれませんが、そうでもありません。

公式ドキュメントにも書いてある通り「このホワイトリストに含まれるアプリは、Doze モード中やアプリ スタンバイ中でもネットワークの使用が可能で、ネットワークの使用が可能で、部分的な wake locksを保持することができます。ただし、ホワイトリストに含まれるアプリにも、他のアプリと同様に他の制限は適用されます。」

AlarmManagerは抑制されるしGPSなども取得できなくなります。

施策3:TimerやAlarmManagerで定期的に画面をONにする

結論から言いますが、駄目になりました。

Android10からバックグラウンドプロセスからのActivity起動に制限が追加されたためActivityを起動して画面をONにすることができなくなりました。

Android9まではこの方法でDozeを回避可能でした。抜け道をAndroid10で潰したということですね。

ただ、Android10で追加されたこの制約は一般ユーザにはメリットです。

ある動画再生アプリが、アプリを利用していない時にも一定間隔で広告画面を全画面表示するというスパム行為をしていたのですが、そのような迷惑行為をできなくする効果があります。

施策4:Push受信時に通知を全画面Intentで表示する

結論から言いますが、Android10以上だとだめです。

電話アプリが利用する全画面での通知表示機能を利用してActivityを起動し、そのActivityで画面をONにします。

Activityは画面をONにしたあとすぐに終了させればユーザの操作には問題ありません。

全画面での通知表示をする条件を画面OFF+バッテリー駆動状態にしておけば、無駄な起動もありません。

MyFirebaseMessagingService.kt

        /* push通知受信時処理は省略 */

        val fullScreenIntent = Intent(context, TransparentActivity::class.java)
        val fullScreenPendingIntent = PendingIntent.getActivity(context, 0,
            fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)

        val builder: NotificationCompat.Builder =
            createNotificationCompatBuilder(context)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(getString(R.string.app_name))
                .setContentText("端末を強制的にWakeupさせました")
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setCategory(NotificationCompat.CATEGORY_CALL)// 着信画面じゃないけど着信カテゴリを設定・・・
                .setFullScreenIntent(fullScreenPendingIntent, true)// 全画面インテントを設定

TransparentActivity.kt
        // 画面ON処理(パーミッションの記載をAndroidManifestに忘れないように)
        wakeLock =
            (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
                newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK
                        or PowerManager.ACQUIRE_CAUSES_WAKEUP, "MyApp::MyWakelockTag").apply {
                    acquire(1000)
                }
            }

AndroidManifest.xml
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

これをPush通知を受けるたびに実行するだけです。送信処理後すぐに端末が受け取るためにはPush通知は重要で送信する必要があります。

ここまですればこのアプリだけDozeを回避することが可能です。というかPush通知を利用している時点で定期的な処理が数十秒可能になります。

本来は画面を強制的にONにすることによって、他のアプリもすべてDozeの影響を受けなくしたかったのですが、Android10, Pixel3aで試したところDoze状態になったログが残りました。

しかし、定期的なPush通知の受信も画面ONもできていたためPushを受信しているアプリは問題なくバックグラウンド処理ができているようです。

なんとも微妙な結果ですが、仕方ありません。Android10からアプリごとにDozeの条件が管理されるようになったのかもしれません。

施策5:AlarmManager.setAlarmClock()を利用する

結論から言うと、この方法でもDoze状態が解除されはするのですが、数十秒後Doze状態に戻ってしまうようです。(Android11, Pixel3aで確認)

ドキュメントを見るとDozeの終了タイミングは下記の4つとなっています。

  • ユーザーによるデバイスの操作
  • デバイスの動き
  • デバイスの画面がオンになる
  • 目覚まし時計のアラームの時刻が近い

施策4でのAlarmManagerで画面をオンにする試みは失敗しましたが、「目覚まし時計のアラームの時刻が近い」という条件は使えそうです。

そこで調べてみるとAlarmManager.setAlarmClock()はDozeから起きるような記述がされています。

ちなみに、Dozeに影響されにくいメソッドとしてAlarmManager.setExactAndAllowWhileIdle()がよく紹介されていますがこちらはそのメソッドを実行しているアプリのみに効果があるだけで、他のアプリのDozeは解除されないそうです。

ということで下記コードでPush通知を受信した5秒後にAlarmをセットするようにしましょう。

MyFirebaseMessagingService.kt
    fun setAlarm(context: Context) {
        // 時間をセットする
        val calendar = Calendar.getInstance()
        // Calendarを使って現在の時間をミリ秒で取得
        calendar.timeInMillis = System.currentTimeMillis()
        // 5秒後に設定
        calendar.add(Calendar.SECOND, 5)

        //明示的なBroadCast
        val intent = Intent(
            applicationContext,
            AlarmReceiver::class.java
        )
        val pendingIntent = PendingIntent.getBroadcast(
            applicationContext, 1, intent, 0
        )

        //アラームクロックインフォを作成してアラーム時間をセット
        val clockInfo = AlarmClockInfo(calendar.timeInMillis, null)
        //アラームマネージャーにアラームクロックをセット
        (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager).setAlarmClock(clockInfo, pendingIntent)
    }
AndroidManifest.xml
    <receiver android:name=".AlarmReceiver"/>
AlarmReceiver.kt
class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        // Doze回避だけが目的なら何も処理しなくてもいいかも
    }
}

これで試したところAndroid11でDozeに入ったあとPush通知を受信するとDozeが解除されることを確認しました。

アプリの都合上Push通知受信後にAlarmを設定していますが、別にPush通知はなくてもいいと思われます。

ただし、AlarmManager.setAlarmClock()を利用するとAndroidの通知領域に目覚まし時計マークが表示されて目覚ましが設定されているのと同じ状態になるのでアプリによっては適さない可能性もあります。

また、Android11で試したところ数十秒でDoze状態に戻っていました。OSで対策がされたのかもしれません。5分間隔でAlarmManager.setAlarmClock()を実行すれば5分ごとにDozeが数十秒解除されるような動作をします。

数十秒の解除中は他のPush通知も遅延なく受信しているようなので、通知遅延の対策としては使えそうです。

施策6:施策4 + 施策5

一つでだめなら2つ合わせてみましょう。全画面Intentで画面をONにしつつ、AlarmManager.setAlarmClock()を使ってみたところ、Doze状態になったあとDozeが解除されてその後1時間はDozeにならないことを確認しました。

ただし、施策5の時と同様にすぐにDozeに戻ってしまうことが何回かあったので、絶対ではないようですがほぼ無効化ができています。

自分で試してみたい方へ

Dozeは、 端末メーカーによって微妙に挙動が変わるためこの結果が確実ではない可能性があります。

信じられない場合は、自身でサンプルアプリを作って試すことをおすすめします。

また、施策6は、下記アプリを入れて確認ができます。

Doze Buster
https://play.google.com/store/apps/details?id=jp.nittan.dozebuster&hl=jp

Doze状態になっているのかどうかのログを残すだけにも利用できるのでどうぞお使いください。

16
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
17