Androidの権限要求処理をReactive Extensions(Rx)で書いてみました

  •  
 
トビウオ2019年11月21日 - 10:21 に投稿

概要

Androidアプリは、「ユーザー エクスペリエンスや端末上のデータに悪影響を及ぼす可能性のあること(公式ドキュメント、原文ママ)」を要求する際、その処理の実行権限(パーミッション)をOSに確認する必要があります。パーミッションといっても様々なものがありますが、主に次の2つに大別されます。

  • Normal パーミッション……アプリのマニフェストに使用する旨を記述していた場合、OSによって自動で承認されるもの。インターネット通信を行うパーミッションなどがこれに該当します

  • Dangerous パーミッション……アプリのマニフェストに使用する旨を記述し、更にプログラム内でパーミッションをOSに要求する必要があるもの。要求されるとOSはユーザーに対し、パーミッションを使用させていいか確認を取ります位置情報を取得するパーミッションなどがこれに該当します

ただ、OSにDangerous パーミッションを確認するコードは、非同期を伴うため記述がめんどくさいので、Reactive Extensionsなどの非同期処理ライブラリでラップして簡潔に済ませたいです。ただ、ラップするだけでも結構な手間であることが試して分かったので、その試行錯誤の記録をメモしておきます。

諸注意:

  • Androidプログラミングで最もよく使われるのはJava or Kotlinだと思われますので、以下の記述ではKotlinを使用します
  • Android 5.1以下の端末 or アプリの targetSdkVersion が 22以下の場合、Dangerous パーミッションを含むアプリについては「インストール時にパーミッションを使用するか確認を取るが実行時に確認を取らない」対応になります。ただ、公式の配信ダッシュボードによると、Android 5.1以下の端末は25%ほどなので、75%の端末への対応を考えると毎回確認を取るべきかと……
  • ぶっちゃけRxPermissionsを使えばいいだけの話ではあるのですが、コレを使うとRxJava2に依存してしまうので、RxJava以外の非同期ライブラリを使いたい人は以下の記述を読みながら適宜スクラッチしましょう

普通に権限を要求する場合

当然ですが、「そのパーミッションを要するAPI」を実行する前に要求する必要があります。以下の記述では、とりあえずActivity#onCreateの中で要求するとしましょう。

まず、指定したパーミッションが既にユーザーに許可されているかを確認します。確認せず毎回getPermission()するのも駄目ではありませんが(許可されていれば自動的に成功するため)、無駄な処理を省きたい場合は確認するべきでしょう。また、より消極的に、「ユーザーが2度とパーミッションを確認されたくない」かどうかを判定するメソッドもあります。

import android.Manifest
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat

// 確認したいパーミッション
val permission = Manifest.permission.ACCESS_FINE_LOCATION

if (ContextCompat.checkSelfPermission( this.applicationContext, permission) == PackageManager.PERMISSION_GRANTED) {
  // ユーザーにパーミッションが許可されている
} else {
  // ユーザーにパーミッションが許可されていない
}

if (ActivityCompat.shouldShowRequestPermissionRationale(this.applicationContext, permission)) {
  // 「確認ダイアログを表示するべきケース」である場合。
  // ・ユーザーにパーミッションを確認したが拒否された。
  //  しかし「二度と表示しない」というチェックはされていない
} else {
  // 「確認ダイアログを表示するべきではないケース」である場合。
  // ・Normal パーミッションである
  // ・Dangerous パーミッションだが、ユーザーが以前に
  //  「二度と表示しない」というチェックを行っている
}

そして、まだ許可をもらっていない場合、改めてパーミッションをリクエストします。すると、onRequestPermissionsResult()メソッドに判定結果がコールバックとして返ってきます。パーミッションの確認時は1個づつなのに要求時は複数個まとめて行えるというのも変な話ですが、仕様なので仕方ありません。

// 要求コード。1以上のInt型の整数ならOK
val PERMISSIONS_REQUEST_CODE = 1

// 要求したいパーミッション
val request_permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.RECORD_AUDIO)

requestPermissions(request_permissions, PERMISSIONS_REQUEST_CODE)

override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    // requestCodeの値によって「どのrequestPermissions()の要求か」を調べる
    when (requestCode) {
        PERMISSIONS_REQUEST_CODE -> {
            if (grantResults.isEmpty()) {
                // grantResultsが空だと、「ユーザーが要求に対して回答しなかった」ということを表す
            } else {
                // grantResultsが空ではない場合、要素数はpermissionsと一緒になる
                // permissionsで指定した各権限について、grantResultsの各要素が「許可したか」を返してくるするイメージ
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                  // 1つ目の結果(permissions[0])について許可された場合
                } {
                  // 1つ目の結果(permissions[0])について許可されなかった場合
                }
                if (grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                  // 2つ目の結果(permissions[1])について許可された場合
                } {
                  // 2つ目の結果(permissions[1])について許可されなかった場合
                }
            }
        }
    }
}

参考資料:

単純にRxでラップした場合

話を簡単にするため、パーミッションを1個づつ要求するとします。そして、Completable型を返す「requestPermission(パーミッション名)」という関数をこれから作っていくことにします。

ただ、onRequestPermissionsResult()メソッドは、Activity or ActivityCompat or Fragment or FragmentCompatクラスのメソッドをoverrideして使用するため、よくある「コールバッククラスをCompletable.create()内で作成して使用」するパターンが使えません。そのため、CompletableSubjectを差し込んで使うことになります。

import android.Manifest
import androidx.appcompat.app.AppCompatActivity
import io.reactivex.Completable
import io.reactivex.subjects.CompletableSubject

class MainActivity : AppCompatActivity() {
  private val PERMISSIONS_REQUEST_CODE = 1
  private val subject = CompletableSubject.create()

  // 権限要求用のメソッド
  fun requestPermission(permission: String): Completable {
    if (checkPermission(permission)) {
      return Completable.complete()
    }
    requestPermissions(permission, PERMISSIONS_REQUEST_CODE)
    return subject
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    // 権限要求
    requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)
    .subscribe {
      // 後続の処理
    }
  }

  // 要求結果
  override fun onRequestPermissionsResult(
  requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    when (requestCode) {
      PERMISSIONS_REQUEST_CODE -> {
          if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
              subject.onComplete()
          } else {
              subject.onError(RuntimeException("必要な権限を取得できませんでした."))
          }
      }
    }
  }
}

すると、確かに目的は達成されるものの、「Rxを使っているのにコールバック部分を別所に書けない」といった不満が生じます。また、requestPermission()メソッド自体も、コールバック関数を書いた場所でしか使うことができません。もうちょっとスッキリ書けないものでしょうか?

Fragmentを噛ませて解決

ここで使う小道具がFragmentです。FragmentはActivityと同様にライフライクルを持ち、UI表示に使えるクラスです。そしてFragmentのインスタンスをActivity内部に追加できるので、ActivityのサイクルでFragmentを初期化し、使用することができます。

ここで注意したいのは、Fragmentを作ったからといって、それが必ずしもViewの表示に影響するとは限らないという点です。そのため、UI表示に影響せず、「内部的にFragmentを利用する」ことができます。

また、Fragmentを間に噛ませることで、次のような処理が行なえます。「権限要求」をFragment経由でサービスクラスに委譲しているとも言えますね。

  • Fragmentクラスを継承したPermissionFragmentクラスを作る(他の名前でもいい)
  • PermissionFragmentクラス内で、checkSelfPermission()による確認・requestPermissions()を使った要求・onRequestPermissionsResult()による結果取得を実施
  • PermissionFragmentクラスによる権限要求の処理結果は、前述のCompletableSubjectを利用して受け取る
  • PermissionFragmentクラス生成〜Activityに追加する部分は、サービスクラスとして切り分ける

実際のプログラミングとしては、次のようになります。まずPermissionFragmentクラスを作成し、

private const val PERMISSIONS_REQUEST_CODE = 1

class PermissionFragment: Fragment() {
  private val subject = CompletableSubject.create()

  fun checkPermission(permission: String): Boolean {
    return ContextCompat.checkSelfPermission(this.context!!, permission) == PackageManager.PERMISSION_GRANTED
}

fun getPermission(permission: String): Completable {
    requestPermissions(arrayOf(permission), PERMISSIONS_REQUEST_CODE)
    return subject
}

override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    when (requestCode) {
      PERMISSIONS_REQUEST_CODE -> {
        if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
          subject.onComplete()
        } else {
          subject.onError(RuntimeException("必要な権限を取得できませんでした."))
        }
      }
    }
  }
}

それをサービスクラスを使って生成・管理します。何度も同じインスタンスを生成しないように注意しましょう。

object PermissionService {
  private val TAG = this.javaClass.simpleName
  private lateinit var permissionFragment: PermissionFragment

  fun initialize(activity: FragmentActivity) {
    val fragment = activity.supportFragmentManager.findFragmentByTag(TAG)
    if (fragment === null) {
      permissionFragment = PermissionFragment()
      activity.supportFragmentManager
        .beginTransaction()
        .add(permissionFragment, TAG)
        .commitNow()
    } else {
      permissionFragment = fragment as PermissionFragment
    }
  }

  fun getPermission(permission: String): Completable {
    if (permissionFragment.checkPermission(permission)) {
      return Completable.complete()
    }
    return permissionFragment.getPermission(permission)
  }
}

そして、起動するActivityで初期化し、

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

    PermissionService.initialize(this)
  }
}

使用したい箇所で使用します、

PermissionService.getPermission(Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe {
  // 後続の処理
}

コメントを追加

プレーンテキスト

  • HTMLタグは利用できません。
  • 行と段落は自動的に折り返されます。
  • ウェブページのアドレスとメールアドレスは自動的にリンクに変換されます。
CAPTCHA
この質問はあなたが人間の訪問者であるかどうかをテストし、自動化されたスパム送信を防ぐためのものです。