はじめに (対象読者・この記事でわかること)
この記事は、Android アプリ開発を行っている Java エンジニア、特に Service から UI の見た目を制御したいと考えている方を対象としています。
Service はバックグラウンドで動作するため、Activity が存在しない状態でもステータスバーの色や明暗を変更したいケースがあります。本記事を読むと、以下ができるようになります。
- Service の
onCreateでステータスバーを暗く(ダーク)設定する方法 - Android 6.0 以降の API で安全にステータスバーのテーマを切り替えるテクニック
- 実装例と、よくある落とし穴の対処法
背景として、Android 12 からはシステム全体でのライト/ダークテーマが推奨される一方、個別の画面や Service で独自にステータスバーを暗くしたいケースが増えてきました。そこで本記事では実装コードと注意点をまとめています。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java による Android アプリ開発の基本(Activity/Service のライフサイクルなど)
- AndroidManifest.xml の基本的な設定方法
- Android SDK の
WindowInsetsControllerやView.SYSTEM_UI_FLAG_...系の API に対する概念的理解
ステータスバーを暗くする概要と背景
Android ではステータスバーの明暗は主に システム UI フラグ と WindowInsetsController を通じて制御できます。通常は Activity の onCreate で Window オブジェクトに対して設定しますが、Service から直接 UI にアクセスすることはできません。そこで WindowManager を利用し、TYPE_APPLICATION_OVERLAY(または TYPE_APPLICATION)のトークンを付与した仮想的なウィンドウを生成し、そのウィンドウのステータスバー属性を変更する手法を取ります。
この手法のメリットは、バックグラウンドで動作する Service が UI の外観を制御できる点です。デメリットとしては、Android 8.0 以降で導入された 「画面上に表示できるオーバーレイの権限」 が必要になる点と、OS バージョンごとの API 差異に注意が必要です。
以下では、実装手順を段階的に解説し、実際に動作させるためのコード例と、開発時に遭遇しやすいエラーや落とし穴の対処法を紹介します。
Service からステータスバーを暗くする実装手順
ステップ 1: 必要な権限とマニフェスト設定
まずは、オーバーレイウィンドウを表示するための権限を AndroidManifest.xml に追加します。
Xml<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <application ...> <service android:name=".DarkStatusBarService" android:exported="false"/> </application>
注意:
SYSTEM_ALERT_WINDOW権限はユーザーが設定画面で手動で許可する必要があります。アプリ起動時に許可を促す UI を用意しておくと親切です。
ステップ 2: Service の雛形作成
次に、ステータスバーを暗くするロジックを持つ Service を作成します。
Javapublic class DarkStatusBarService extends Service { private WindowManager windowManager; private View overlayView; @Override public void onCreate() { super.onCreate(); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); createOverlay(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 必要ならばインテントで切り替え指示を受け取る return START_STICKY; } @Override public void onDestroy() { super.onDestroy(); if (overlayView != null) { windowManager.removeView(overlayView); } } @Nullable @Override public IBinder onBind(Intent intent) { return null; } /** オーバーレイビューを生成し、ステータスバーを暗く設定 */ private void createOverlay() { // 透明な 1dp のビューを作成 overlayView = new View(this); overlayView.setLayoutParams(new WindowManager.LayoutParams( 1, 1, // Android O 以降は TYPE_APPLICATION_OVERLAY が必須 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT)); // オーバーレイを追加 windowManager.addView(overlayView, overlayView.getLayoutParams()); // UI フラグを設定してステータスバーを暗く if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11 以降は WindowInsetsController が推奨 WindowInsetsController controller = overlayView.getWindowInsetsController(); if (controller != null) { controller.setSystemBarsAppearance( WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS); // LIGHT_STATUS_BARS を外すと暗くなる int flags = controller.getSystemBarsAppearance() & ~WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; controller.setSystemBarsAppearance(flags, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS); } } else { // それ以前はデコレーションフラグで制御 overlayView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); // API 23 (Marshmallow) 以上は `View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR` が使用可能 // これを付けないことで暗いステータスバーになる } } }
ポイント解説
-
オーバーレイビューのサイズ
1dp の透明ビューを作成し、FLAG_NOT_FOCUSABLEとFLAG_NOT_TOUCHABLEにしてユーザー操作を遮らないようにします。サイズは実質的に意味がなく、ステータスバー制御だけのために最低限のリソースで済ませます。 -
Window Type の選択
Android O(API 26)以降はTYPE_APPLICATION_OVERLAYが必須です。古い端末向けにフォールバックしていますが、実際にサポート対象を絞る場合は API 26 以上に限定してください。 -
SystemBarsAppearance の利用
Android 11(API 30)からはWindowInsetsControllerが導入され、ライト/ダーク の外観をビットマスクで制御できます。APPEARANCE_LIGHT_STATUS_BARSを除外することで暗いステータスバーになります。 -
下位互換
API 23 以上であればSYSTEM_UI_FLAG_LIGHT_STATUS_BARが利用可能です。ただし、暗くしたいのでこのフラグは付与しません。SYSTEM_UI_FLAG_LAYOUT_FULLSCREENと併せてステータスバー領域への描画を許可します。
ステップ 3: 権限取得と Service 起動
ユーザーにオーバーレイ権限を許可させるためのコード例です。
Javapublic class MainActivity extends AppCompatActivity { private static final int REQUEST_OVERLAY_PERMISSION = 1001; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION); } else { startDarkStatusBarService(); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_OVERLAY_PERMISSION) { if (Settings.canDrawOverlays(this)) { startDarkStatusBarService(); } else { Toast.makeText(this, "オーバーレイ権限が必要です", Toast.LENGTH_SHORT).show(); } } } private void startDarkStatusBarService() { Intent serviceIntent = new Intent(this, DarkStatusBarService.class); ContextCompat.startForegroundService(this, serviceIntent); } }
ポイント: Android 8.0 以降はバックグラウンドで Service を開始する際に
startForegroundServiceが推奨されます。実装上は通知チャンネルを作成し、フォアグラウンド通知を表示させる必要がありますが、ここではコード簡潔化のため省略しています。実際のプロダクトでは必ず通知を付与してください。
ハマった点やエラー解決
1. android.view.WindowManager$BadTokenException が発生
症状:
addView時に "Unable to add window -- token null is not valid; is your activity running?" と例外が出る。
原因: WindowManager に渡す LayoutParams に TYPE_APPLICATION_OVERLAY 以外のタイプを使用した、または SYSTEM_ALERT_WINDOW 権限が許可されていない。
対策:
- TYPE_APPLICATION_OVERLAY を必ず指定(API 26 以上)。
- Settings.canDrawOverlays(context) で権限取得状態を確認し、許可がなければ設定画面へ遷移させる。
2. ステータスバーが暗くならない
症状: Service 起動後もステータスバーはライト(白文字)状態のまま。
原因: WindowInsetsController が null を返すケース。オーバーレイビューがウィンドウに結びついていないか、FLAG_LAYOUT_IN_SCREEN が不足している。
対策:
- FLAG_LAYOUT_IN_SCREEN を WindowManager.LayoutParams に追加。
- Android 10 以下では SYSTEM_UI_FLAG_LIGHT_STATUS_BAR を 設定しない ことが重要。代わりに View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN を有効にする。
3. フォアグラウンドサービスが即終了する
症状:
startForegroundServiceの呼び出し直後に Service がonDestroyへ遷移。
原因: フォアグラウンド通知を作成し startForeground(id, notification) を呼び出していないため、OS が Service を停止させる。
対策:
Javaprivate void startForegroundService() { NotificationChannel channel = new NotificationChannel( "dark_status_bar", "ステータスバー暗転", NotificationManager.IMPORTANCE_LOW); getSystemService(NotificationManager.class).createNotificationChannel(channel); Notification notification = new NotificationCompat.Builder(this, "dark_status_bar") .setContentTitle("ステータスバー暗転中") .setSmallIcon(R.drawable.ic_stat_name) .setOngoing(true) .build(); startForeground(1, notification); }
このコードを Service の onCreate で呼び出すと、OS により強制終了されなくなります。
まとめ
本記事では Service からステータスバーを暗くする具体的な実装手順 を解説しました。
- 権限設定とオーバーレイウィンドウの作成:
SYSTEM_ALERT_WINDOWとTYPE_APPLICATION_OVERLAYが必須 - WindowInsetsController / UI フラグで暗色化:API 30 以降は
setSystemBarsAppearance、それ以前はSYSTEM_UI_FLAG_LAYOUT_FULLSCREENを利用 - ハマりやすいポイントと対策:
BadTokenException、暗転が反映されない、フォアグラウンド通知の欠如
これらを実装すれば、バックグラウンドで動作する Service でもステータスバーを暗くし、アプリ全体の UI が統一感のあるダークテーマになるため、ユーザー体験が向上します。今後は、テーマ切替をリアルタイムで行う 方法や、Jetpack Compose と組み合わせた実装例 についても取り上げる予定です。
参考資料
- Android Developers – WindowInsetsController
- Android Developers – System UI Visibility
- Android Developers – Overlay Permission
- 《Android Programming: The Big Nerd Ranch Guide》 – 第 7 章 UI カスタマイズ
