はじめに

Androidアプリ開発において、システムの電源メニュー(再起動、電源オフなどのダイアログ)をプログラムから表示したいというニーズがあります。通常、この電源メニューは電源ボタンの長押しで表示されますが、アクセシビリティサービスを利用することで、アプリからこのメニューを呼び出すことが可能です。

本記事では、AccessibilityServiceGLOBAL_ACTION_POWER_DIALOGを使用して、システムの電源メニューを表示する方法を詳しく解説します。

想定される用途

  • クイック設定タイルからの電源メニュー表示
  • ジェスチャー操作による電源メニュー起動
  • 物理ボタンが故障した端末での代替操作
  • カスタムランチャーでの電源管理機能

この記事で学べること

  • アクセシビリティサービスの基本的な実装方法
  • グローバルアクションを使ったシステム機能の呼び出し
  • ユーザーガイダンスの実装方法

開発環境

本記事で使用した開発環境は以下の通りです:

項目バージョン/内容
IDEAndroid Studio Ladybug | 2024.2.1 以降
言語Kotlin 1.9.20 以降
Gradle8.x
compileSdkAPI 36 (Android 15)
minSdkAPI 26 (Android 8.0)
targetSdkAPI 36 (Android 15)

主な依存関係

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
}

必要な知識

  • Kotlinの基本文法
  • Androidの基本的なコンポーネント(Activity、Service)
  • Intentの使い方

実装説明

アーキテクチャ概要

システムの電源メニューを表示するには、以下の3つのコンポーネントが必要です:

┌─────────────────┐
│  MainActivity   │ ← ユーザーインターフェース
└────────┬────────┘
         │
         ↓
┌─────────────────────────┐
│ AccessibilityHelper     │ ← ヘルパークラス(状態チェック)
└────────┬────────────────┘
         │
         ↓
┌──────────────────────────────┐
│ PowerAccessibilityService    │ ← アクセシビリティサービス
└──────────────────────────────┘
         │
         ↓
┌──────────────────────────────┐
│ GLOBAL_ACTION_POWER_DIALOG   │ ← システムAPI
└──────────────────────────────┘

処理フロー

  1. ユーザーアクション: ユーザーがボタンをタップ
  2. サービスチェック: アクセシビリティサービスが有効か確認
  3. Intent送信: 有効な場合、Serviceに処理を依頼
  4. グローバルアクション実行performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)を呼び出し
  5. 結果: システムの電源メニューが表示される

重要なポイント

なぜアクセシビリティサービスが必要か?

通常のアプリには、システムレベルの機能(電源メニューなど)にアクセスする権限がありません。しかし、AccessibilityServiceは障害を持つユーザーを支援するための特権を持っており、その一環としてperformGlobalAction()を使用できます。

セキュリティ上の考慮事項

  • アクセシビリティサービスは強力な権限を持つため、ユーザーが手動で有効化する必要がある
  • Google Playでの公開時は、なぜこの権限が必要かを明確に説明する必要がある
  • 悪用を防ぐため、最小限の機能のみを実装することが推奨される

実装

それでは、実際のコードを見ていきましょう。

1. アクセシビリティサービスの実装

まず、アクセシビリティサービスを継承したクラスを作成します。

PowerAccessibilityService.kt

package jp.upft.systempowerdialogapp

import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.util.Log
import android.view.accessibility.AccessibilityEvent

class PowerAccessibilityService : AccessibilityService() {

    companion object {
        private const val TAG = "PowerAccessService"
        const val ACTION_POWER_DIALOG = "jp.upft.systempowerdialogapp.action.POWER_DIALOG"
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        // このアプリでは特にイベント処理は不要
    }

    override fun onInterrupt() {
        Log.d(TAG, "Service interrupted")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "onStartCommand called")
        
        intent?.action?.let { action ->
            Log.d(TAG, "Action received: $action")
            
            if (action == ACTION_POWER_DIALOG) {
                Log.d(TAG, "Performing GLOBAL_ACTION_POWER_DIALOG")
                val success = performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)
                Log.d(TAG, "Power dialog result: $success")
            }
        }
        
        return super.onStartCommand(intent, flags, startId)
    }
}

ポイント:

  • onStartCommand(): Intentを受け取り、カスタムアクションを処理
  • performGlobalAction(GLOBAL_ACTION_POWER_DIALOG): システムの電源メニューを表示
  • ログ出力: デバッグやトラブルシューティングに役立つ

2. ヘルパークラスの実装

サービスの状態チェックと電源メニュー表示のロジックをまとめたヘルパークラスを作成します。

AccessibilityHelper.kt

package jp.upft.systempowerdialogapp

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import android.widget.Toast

object AccessibilityHelper {

    /**
     * アクセシビリティサービスが有効かチェック
     */
    fun isServiceEnabled(context: Context): Boolean {
        val manager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
            ?: return false

        val serviceInfoList = manager.getEnabledAccessibilityServiceList(
            AccessibilityServiceInfo.FEEDBACK_ALL_MASK
        )

        val packageName = context.packageName
        val serviceName = PowerAccessibilityService::class.java.name

        return serviceInfoList.any { serviceInfo ->
            val info = serviceInfo.resolveInfo.serviceInfo
            info.packageName == packageName && info.name == serviceName
        }
    }

    /**
     * パワーダイアログを表示
     */
    fun showPowerDialog(context: Context): Boolean {
        if (!isServiceEnabled(context)) {
            showServiceDisabledMessage(context)
            return false
        }

        return try {
            val intent = Intent(context, PowerAccessibilityService::class.java).apply {
                action = PowerAccessibilityService.ACTION_POWER_DIALOG
            }
            context.startService(intent)
            true
        } catch (e: Exception) {
            Toast.makeText(
                context,
                "エラー: ${e.message}",
                Toast.LENGTH_SHORT
            ).show()
            false
        }
    }

    /**
     * サービス無効メッセージを表示して設定画面へ誘導
     */
    private fun showServiceDisabledMessage(context: Context) {
        Toast.makeText(
            context,
            "アクセシビリティサービスを有効にしてください",
            Toast.LENGTH_LONG
        ).show()
        openAccessibilitySettings(context)
    }

    /**
     * アクセシビリティ設定画面を開く
     */
    fun openAccessibilitySettings(context: Context) {
        try {
            val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            context.startActivity(intent)
        } catch (e: Exception) {
            Toast.makeText(
                context,
                "設定画面を開けませんでした",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}

ポイント:

  • isServiceEnabled(): サービスの有効性を確認
  • showPowerDialog(): 電源メニュー表示のメインロジック
  • エラーハンドリング: try-catchでエラーを適切に処理
  • ユーザーガイダンス: サービスが無効な場合は設定画面へ誘導

3. メインアクティビティの実装

ユーザーインターフェースを持つメインアクティビティを実装します。

MainActivity.kt

package jp.upft.systempowerdialogapp

import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // パワーダイアログを表示するボタン
        findViewById<Button>(R.id.btnShowPowerDialog).setOnClickListener {
            AccessibilityHelper.showPowerDialog(this)
        }

        // サービス状態をチェックするボタン
        findViewById<Button>(R.id.btnCheckService).setOnClickListener {
            val isEnabled = AccessibilityHelper.isServiceEnabled(this)
            val message = if (isEnabled) {
                "アクセシビリティサービスは有効です"
            } else {
                "アクセシビリティサービスは無効です"
            }
            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
        }

        // 設定画面を開くボタン
        findViewById<Button>(R.id.btnOpenSettings).setOnClickListener {
            AccessibilityHelper.openAccessibilitySettings(this)
        }
    }

    override fun onResume() {
        super.onResume()
        // 画面に戻ったときにサービス状態を確認
        checkServiceStatus()
    }

    private fun checkServiceStatus() {
        if (!AccessibilityHelper.isServiceEnabled(this)) {
            Toast.makeText(
                this,
                "アクセシビリティサービスが無効です",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}

ポイント:

  • 3つのボタン: 電源メニュー表示、状態確認、設定画面起動
  • onResume(): アプリに戻った際にサービス状態を再チェック
  • シンプルなUI: ユーザーにとって分かりやすいインターフェース

4. レイアウトファイル

シンプルで使いやすいUIを作成します。

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_title"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="32dp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/app_description"
        android:textSize="14sp"
        android:gravity="center"
        android:layout_marginBottom="32dp" />

    <Button
        android:id="@+id/btnShowPowerDialog"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/btn_show_power_dialog"
        android:layout_marginBottom="16dp" />

    <Button
        android:id="@+id/btnCheckService"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/btn_check_service"
        android:layout_marginBottom="16dp" />

    <Button
        android:id="@+id/btnOpenSettings"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/btn_open_settings" />

</LinearLayout>

5. アクセシビリティサービス設定

サービスの動作を定義するXMLファイルを作成します。

res/xml/accessibility_service_config.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:description="@string/accessibility_service_description"
    android:summary="@string/accessibility_service_summary" />

6. 文字列リソース

res/values/strings.xml

<resources>
    <string name="app_name">SystemPowerDialogApp</string>
    
    <!-- UI -->
    <string name="app_title">システムパワーダイアログ</string>
    <string name="app_description">電源メニューを表示するアプリです。\n使用するにはアクセシビリティサービスを有効にしてください。</string>
    <string name="btn_show_power_dialog">電源メニューを表示</string>
    <string name="btn_check_service">サービス状態を確認</string>
    <string name="btn_open_settings">アクセシビリティ設定を開く</string>
    
    <!-- アクセシビリティサービス関連 -->
    <string name="accessibility_service_label">パワーダイアログサービス</string>
    <string name="accessibility_service_summary">電源メニューを表示するためのサービス</string>
    <string name="accessibility_service_description">このアプリがシステムの電源メニューを表示できるようにします。\n個人情報の収集や他のアプリの操作は行いません。</string>
</resources>

ポイント:

  • description: ユーザーがサービスを有効にする際に表示される説明文
  • セキュリティとプライバシーへの配慮を明記

7. AndroidManifest.xml

サービスをシステムに登録します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SystemPowerDialogApp">
        
        <!-- メインアクティビティ -->
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- アクセシビリティサービス(重要) -->
        <service
            android:name=".PowerAccessibilityService"
            android:label="@string/accessibility_service_label"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="false">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

    </application>

</manifest>

重要な設定:

  • android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE": 必須のパーミッション
  • android:exported="false": セキュリティのため、外部からの起動を禁止
  • <intent-filter>: アクセシビリティサービスとして登録
  • <meta-data>: サービス設定ファイルを参照

結果

実行手順

  1. アプリを起動
  2. アクセシビリティ設定を開く
    • 「パワーダイアログサービス」をタップ
    • トグルスイッチをONにする
    • 警告ダイアログで「許可」をタップ
    • トップに戻る
  3. 電源メニューを表示
    • 2. 終了後、「電源メニューを表示」をタップ
    • システムの電源メニューが表示される

実行動画

ログ出力

Logcatで以下のようなログが確認できます:

D/PowerAccessService: onStartCommand called
D/PowerAccessService: Action received: jp.upft.systempowerdialogapp.action.POWER_DIALOG
D/PowerAccessService: Performing GLOBAL_ACTION_POWER_DIALOG
D/PowerAccessService: Power dialog result: true

トラブルシューティング

電源メニューが表示されない場合

原因1: アクセシビリティサービスが有効になっていない

  • 確認箇所: 「サービス状態を確認」ボタンで状態をチェックし、必要に応じて設定画面から有効化

原因2: サービスの登録が正しくない

  • 確認箇所: AndroidManifest.xmlの設定を再確認

原因3: 端末の制限

  • 確認箇所: 一部の端末メーカーはカスタムROMでこの機能を制限している場合がある

まとめ

本記事では、Androidのアクセシビリティサービスを利用して、システムの電源メニューをプログラムから表示する方法を解説しました。

学んだこと

  • ✅ アクセシビリティサービスの基本的な実装方法
  • ✅ GLOBAL_ACTION_POWER_DIALOGの使い方
  • ✅ サービスの状態チェックとユーザーガイダンスの重要性
  • ✅ AndroidManifest.xmlでのサービス登録方法

応用例

この技術を応用すると、以下のような機能も実装できます:

  1. クイック設定タイル: 通知パネルから電源メニューを起動
  2. ウィジェット: ホーム画面ウィジェットからの起動
  3. ジェスチャー: スワイプやタップジェスチャーでの起動
  4. 他のグローバルアクション:
    • GLOBAL_ACTION_NOTIFICATIONS: 通知パネルを開く
    • GLOBAL_ACTION_RECENTS: 最近のアプリを表示
    • GLOBAL_ACTION_HOME: ホーム画面に戻る

セキュリティとプライバシーへの配慮

アクセシビリティサービスは強力な権限を持つため、以下の点に注意が必要です:

  1. 透明性: ユーザーに何をするサービスか明確に説明する
  2. 最小権限の原則: 必要最小限の機能のみを実装する
  3. データ保護: 個人情報を収集しない
  4. Google Playポリシー: 審査基準を満たす説明とプライバシーポリシーを提供

Google Playでの公開について

アクセシビリティサービスを使用するアプリをGoogle Playで公開する場合は、以下の準備が必要です:

  • アプリの主要機能としてアクセシビリティ機能を実装する
  • なぜアクセシビリティサービスが必要かを明確に説明する
  • 適切なプライバシーポリシーを提供する
  • スクリーンショットや動画で使用方法を示す

参考リンク

おわりに

この実装により、ユーザーにとってより便利なアプリ体験を提供できます。ただし、強力な権限を持つ機能であるため、責任を持って適切に使用することが重要です。

アクセシビリティサービスは本来、障害を持つユーザーを支援するための機能です。この技術を使用する際は、その本来の目的を尊重し、ユーザーのプライバシーとセキュリティを最優先に考えて開発を進めましょう。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる