はじめに (対象読者・この記事でわかること)

この記事は、Android アプリ開発を行っている Java エンジニア、特に Service から UI の見た目を制御したいと考えている方を対象としています。
Service はバックグラウンドで動作するため、Activity が存在しない状態でもステータスバーの色や明暗を変更したいケースがあります。本記事を読むと、以下ができるようになります。

  • Service の onCreate でステータスバーを暗く(ダーク)設定する方法
  • Android 6.0 以降の API で安全にステータスバーのテーマを切り替えるテクニック
  • 実装例と、よくある落とし穴の対処法

背景として、Android 12 からはシステム全体でのライト/ダークテーマが推奨される一方、個別の画面や Service で独自にステータスバーを暗くしたいケースが増えてきました。そこで本記事では実装コードと注意点をまとめています。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Java による Android アプリ開発の基本(Activity/Service のライフサイクルなど)
  • AndroidManifest.xml の基本的な設定方法
  • Android SDK の WindowInsetsControllerView.SYSTEM_UI_FLAG_... 系の API に対する概念的理解

ステータスバーを暗くする概要と背景

Android ではステータスバーの明暗は主に システム UI フラグWindowInsetsController を通じて制御できます。通常は Activity の onCreateWindow オブジェクトに対して設定しますが、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 を作成します。

Java
public 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` が使用可能 // これを付けないことで暗いステータスバーになる } } }

ポイント解説

  1. オーバーレイビューのサイズ
    1dp の透明ビューを作成し、FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCHABLE にしてユーザー操作を遮らないようにします。サイズは実質的に意味がなく、ステータスバー制御だけのために最低限のリソースで済ませます。

  2. Window Type の選択
    Android O(API 26)以降は TYPE_APPLICATION_OVERLAY が必須です。古い端末向けにフォールバックしていますが、実際にサポート対象を絞る場合は API 26 以上に限定してください。

  3. SystemBarsAppearance の利用
    Android 11(API 30)からは WindowInsetsController が導入され、ライト/ダーク の外観をビットマスクで制御できます。APPEARANCE_LIGHT_STATUS_BARS を除外することで暗いステータスバーになります。

  4. 下位互換
    API 23 以上であれば SYSTEM_UI_FLAG_LIGHT_STATUS_BAR が利用可能です。ただし、暗くしたいのでこのフラグは付与しません。SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN と併せてステータスバー領域への描画を許可します。

ステップ 3: 権限取得と Service 起動

ユーザーにオーバーレイ権限を許可させるためのコード例です。

Java
public 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 に渡す LayoutParamsTYPE_APPLICATION_OVERLAY 以外のタイプを使用した、または SYSTEM_ALERT_WINDOW 権限が許可されていない。

対策:
- TYPE_APPLICATION_OVERLAY を必ず指定(API 26 以上)。
- Settings.canDrawOverlays(context) で権限取得状態を確認し、許可がなければ設定画面へ遷移させる。

2. ステータスバーが暗くならない

症状: Service 起動後もステータスバーはライト(白文字)状態のまま。

原因: WindowInsetsController が null を返すケース。オーバーレイビューがウィンドウに結びついていないか、FLAG_LAYOUT_IN_SCREEN が不足している。

対策:
- FLAG_LAYOUT_IN_SCREENWindowManager.LayoutParams に追加。
- Android 10 以下では SYSTEM_UI_FLAG_LIGHT_STATUS_BAR設定しない ことが重要。代わりに View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN を有効にする。

3. フォアグラウンドサービスが即終了する

症状: startForegroundService の呼び出し直後に Service が onDestroy へ遷移。

原因: フォアグラウンド通知を作成し startForeground(id, notification) を呼び出していないため、OS が Service を停止させる。

対策:

Java
private 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_WINDOWTYPE_APPLICATION_OVERLAY が必須
  • WindowInsetsController / UI フラグで暗色化:API 30 以降は setSystemBarsAppearance、それ以前は SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN を利用
  • ハマりやすいポイントと対策BadTokenException、暗転が反映されない、フォアグラウンド通知の欠如

これらを実装すれば、バックグラウンドで動作する Service でもステータスバーを暗くし、アプリ全体の UI が統一感のあるダークテーマになるため、ユーザー体験が向上します。今後は、テーマ切替をリアルタイムで行う 方法や、Jetpack Compose と組み合わせた実装例 についても取り上げる予定です。

参考資料