【Android】 パーミッション判定の複雑なコードを解決!PermissionsDispatcherライブラリを試してみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

モバイルアプリサービス部の浜田です!

AndroidアプリのRuntime Permissionの対応を行ったときに、パーミッションの状態を判定するコードがあふれてしまって、収拾がつかなくなった経験はありますか?

今回は、パーミッションの状態に応じて処理を実行してくれる「PermissionsDispatcher」というライブラリを試してみたので、使用方法などをまとめておきます。

PermissionsDispatcherライブラリとは

PermissionsDispatcherは、AndroidアプリのRuntime Permission対応のためのライブラリです。このライブラリが提供してくれるアノテーションのおかげで、パーミッションの状態に対する処理が宣言的に実装できます。

アノテーション 必須 概要
@RuntimePermissions パーミッションを処理するActivityFragmentに付ける
@NeedsPermission パーミッションが必要な処理を実行する関数に付ける
@OnShowRationale ユーザーにパーミッションが必要な理由を説明するときに呼び出される関数に付ける
@OnPermissionDenied ユーザーがパーミッションが付与しなかったときに呼び出される関数に付ける
@OnNeverAskAgain ユーザーがパーミッションの要求ダイアログで「今後表示しない」を選択したときに呼び出される関数に付ける

環境

Android Studio

Android Studio 3.2.1
Build #AI-181.5540.7.32.5056338, built on October 9, 2018
JRE: 1.8.0_152-release-1136-b06 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.14

動作検証端末

  • エミュレータ
  • Pixel
  • Android 9.0 (API 28) x86

使用するライブラリのバージョン

2018年10月20日にPermissionsDispatcherライブラリのバージョン4.0.0がリリースされていますが、README.mdに記載されているとおり、Jetpackの環境のみサポートしています。

NOTE: 4.x is still alpha and it only supports Jetpack. If you use appcompat 3.x which is almost stable is the way to go.

この記事では非Jetpackの環境を対象として、以下のドキュメントを参考にバージョン3.3.1を使用します。

実装例

1. 新規プロジェクトの作成

まず、Android Studioを使用して新規プロジェクトを作成します。 以下の設定で、アプリケーション名などは任意のものを指定してください。

  • Create Android Project:
    • include Kotlin support: ON
  • Target Android Devices:
    • Phone and Tablet: ON
  • Add an Activity to Mobile:
    • Empty Activityを選択
  • Configure Activity:
    • Activity Name: MainActivityを入力
      • Generate Layout File: ON
    • Layout Name: activity_mainを入力
      • Backwards Compatibility (AppCompat): ON

2. ファイルの内容を変更

新規プロジェクトの作成後、以下のファイルを変更します。

  • appモジュールのbuild.gradle
  • AndroidManifest.xml
  • activity_main.xml
  • MainActivity.kt

appモジュールのbuild.gradle

PermissionsDispatcherライブラリを使用するために、appモジュールのbuild.gradleに以下の内容を追加します。

apply plugin: 'kotlin-kapt'

dependencies {
  implementation("com.github.hotchemi:permissionsdispatcher:3.3.1") {
    // Fragmentで使用したい場合はこの記述を外す
    exclude module: "support-v13"
  }
  kapt "com.github.hotchemi:permissionsdispatcher-processor:3.3.1"
}

AndroidManifest.xml

例として、MainActivity.ktに連絡先の登録数を表示する処理を実装します。 連絡先の取得にはREAD_CONTACTSパーミッションが必要なので、AndroidManifest.xmlに以下の内容を追加しておきます。

<uses-permission android:name="android.permission.READ_CONTACTS" />

activity_main.xml

パーミッションが必要な処理を実行するボタンを設置します。 ボタンがタッチされたときの処理をMainActivity.ktで設定するために、android:id@+id/buttonを指定しています。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:text="連絡先の登録数を表示"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</android.support.constraint.ConstraintLayout>

こんな感じの単純なレイアウトです。

MainActivity.kt

MainActivityを以下の内容に変更してください。

@RuntimePermissions
class MainActivity : AppCompatActivity() {

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

        button.setOnClickListener {
            // 自動生成された関数にパーミッションが必要な処理の呼び出しを委譲
            showContactsWithPermissionCheck()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // 自動生成された関数にパーミッション・リクエストの結果に応じた処理の呼び出しを委譲
        onRequestPermissionsResult(requestCode, grantResults)
    }

    /**
     * 連絡先の登録数をトーストで表示する。
     */
    @NeedsPermission(Manifest.permission.READ_CONTACTS)
    fun showContacts() {
        contentResolver.query(
            ContactsContract.Contacts.CONTENT_URI,
            null, null, null, null
        ).use {
            Toast.makeText(this@MainActivity, "登録数: ${it.count}", Toast.LENGTH_SHORT).show()
        }
    }

    @OnPermissionDenied(Manifest.permission.READ_CONTACTS)
    fun onContactsDenied() {
        Toast.makeText(this, "「許可しない」が選択されました", Toast.LENGTH_SHORT).show()
    }

    @OnShowRationale(Manifest.permission.READ_CONTACTS)
    fun showRationaleForContacts(request: PermissionRequest) {
        AlertDialog.Builder(this)
            .setPositiveButton("許可") { _, _ -> request.proceed() }
            .setNegativeButton("許可しない") { _, _ -> request.cancel() }
            .setCancelable(false)
            .setMessage("登録数を取得するために連絡先にアクセスする必要があります。")
            .show()
    }

    @OnNeverAskAgain(Manifest.permission.READ_CONTACTS)
    fun onContactsNeverAskAgain() {
        Toast.makeText(this, "「今後表示しない」が選択されました", Toast.LENGTH_SHORT).show()
    }
}

実装の解説

1. @RuntimePermissionsアノテーション

パーミッションを処理するActivity@RuntimePermissionsアノテーションを付けます。

@RuntimePermissions
class MainActivity : AppCompatActivity() {
  // ...
}

2. @NeedsPermissionアノテーション

パーミッションが必要な処理を実行する関数に@NeedsPermissionアノテーションを付けます。

    @NeedsPermission(Manifest.permission.READ_CONTACTS)
    fun showContacts() {
      // ...
    }

3. @OnShowRationaleアノテーション

Androidの公式ドキュメントで次のように解説されています。

実行時のパーミッション リクエスト  |  Android Developers

ユーザーがパーミッションを必要とする機能を使おうとしながら、パーミッション リクエストを拒否し続けている場合、ユーザーは、その機能を利用するにはアプリにパーミッションが必要であることを理解していない可能性があります。このような場合は、ユーザーに対して説明を表示するとよいでしょう。

このような状況を判断するために、ActivityCompat.shouldShowRequestPermissionRationale()という関数がサポートライブラリで提供されており、この関数がtrueを返す状況で、OnShowRationaleアノテーションを付けた関数が呼び出されます。

    @OnShowRationale(Manifest.permission.READ_CONTACTS)
    fun showRationaleForContacts(request: PermissionRequest) {
        // ...
    }

4. @OnPermissionDeniedアノテーション

ユーザーがパーミッションが付与しなかったときに呼び出したい関数に@OnPermissionDeniedアノテーションを付けます。

    @OnPermissionDenied(Manifest.permission.READ_CONTACTS)
    fun onContactsDenied() {
      // ...
    }

5. @OnNeverAskAgainアノテーション

ユーザーがパーミッションの要求ダイアログで「今後表示しない」を選択したときに呼び出される関数に@OnNeverAskAgainアノテーションを付けます。

    @OnNeverAskAgain(Manifest.permission.READ_CONTACTS)
    fun onContactsNeverAskAgain() {
      // ...
    }

6. 処理の委譲

アノテーションの指定に対して、PermissionsDispatcherライブラリが次のような関数やクラスを裏で生成してくれます。

fun MainActivity.showContactsWithPermissionCheck() {
    // ...
}

fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
    // ...
}

private class MainActivityShowContactsPermissionRequest(target: MainActivity) : PermissionRequest {
    // ...
  override fun proceed() {
    // ...
  }

  override fun cancel() {
    // ...
  }
}

MainActivity.showContactsWithPermissionCheck()@NeedsPermissionアノテーションを付けたshowContacts()に対して生成された拡張関数です。showContacts()を直接呼び出す代わりに、この生成された関数を呼び出すことで、パーミッションが付与されている場合は処理を実行し、パーミッションが付与されていない場合は状況に応じて適切な処理を呼び出してくれます。

使用例では、ボタンがタッチされたときにこの関数を呼び出しています。

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

        button.setOnClickListener {
            // 自動生成された関数にパーミッションが必要な処理の呼び出しを委譲
            showContactsWithPermissionCheck()
        }
    }

MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray)はパーミッション・リクエストの結果を委譲するための拡張関数です。

使用例では、オーバーライドしたonRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray)の中で単純に委譲しています。

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // 自動生成された関数にパーミッション・リクエストの結果に応じた処理の呼び出しを委譲
        onRequestPermissionsResult(requestCode, grantResults)
    }

MainActivityShowContactsPermissionRequestクラスは、@OnShowRationaleアノテーションを付けたshowRationaleForContacts()に対して生成されたものです。

    @OnShowRationale(Manifest.permission.READ_CONTACTS)
    fun showRationaleForContacts(request: PermissionRequest) {
        AlertDialog.Builder(this)
            .setPositiveButton("許可") { _, _ -> request.proceed() }
            .setNegativeButton("許可しない") { _, _ -> request.cancel() }
            .setCancelable(false)
            .setMessage("登録数を取得するために連絡先にアクセスする必要があります。")
            .show()
    }

@OnShowRationaleアノテーションを付けた関数の引数にPermissionRequestオブジェクトが渡されます。

proceed()メソッドの呼び出しで、アノテーションの引数で指定しているパーミッションの再要求が実行され、cancel()メソッドの呼び出しで、@OnPermissionDeniedアノテーションが付けられた関数が実行されます。

動作確認

パーミッションが付与されているときの動作

パーミッションが付与されている状況でボタンがタッチされたときはすぐにshowContacts()が実行され、連絡先の登録数がトーストで表示されます。

パーミッションが付与されていないときの動作

インストール後初めてのパーミッション・リクエスト

パーミッションが付与されていない状況で、初めてボタンがタッチされたときは次のようなダイアログがOSによって表示されます。

「許可」が選択された場合:

パーミッションが付与され、すぐにshowContacts()が実行され、連絡先の登録数がトーストで表示されます。

「許可しない」が選択された場合:

パーミッションは付与されず、@OnPermissionDeniedアノテーションを付けた関数が実行されます。

一度拒否されたあとのパーミッション・リクエスト

さきほどのダイアログで「許可しない」を選択したあとで、もう一度ボタンをタッチすると、@OnShowRationaleアノテーションを付けた関数が実行され、例として独自に実装しているダイアログが表示されます。

「許可」が選択された場合:

パーミッション・リクエストが実行され、OSによってダイアログが表示されます(初回と異なり、「今後表示しない」というチェックボックスが追加されます)。

「許可しない」が選択された場合:

パーミッションは付与されず、@OnPermissionDeniedアノテーションを付けた関数が実行されます。

「今後表示しない」が選択された場合

OSによって表示されたパーミッション・リクエストのダイアログで、ユーザーが「今後表示しない」を選択した場合、それ以降は常に@OnNeverAskAgainアノテーションを付けた関数が実行されます。

Androidの公式ドキュメントに記載されているとおり、この状況でアプリがパーミッション・リクエストを実行することはできません。

If the user checks the Never ask again box and taps Deny, the system no longer prompts the user if you later attempt to requests the same permission.

この状況で、パーミッションが必要な機能の使用をユーザーが試みたときは、設定アプリから手動でパーミッションを許可するように促すと良いです。

さいごに

以前、サポートライブラリのAPIを直接使用してRuntime Permission対応したときに、パーミッションの状態に対して実行される処理が不明確なコードを書いてしまったことがあります…。

PermissionsDispatcherライブラリを使用することで、ユーザーへの説明など、本質的な仕様の検討に集中できる感触がありました。ぜひ、みなさんも試してみてください!

参考文献