2015/05/31

新しいパーミッションモデル - Runtime Permission

はじめに.

caution!
本稿はAndroid M Preview向けに限られた内容です.
本稿の一部または全てはM Preview以前のAndroid versionやM正式版で有効ではない可能性があります.

本稿の一部はPermissionsの内容を参考としています. より正確な情報を得たい場合はこちらもご参考ください.
また, 本稿の説明で例示しているソースコードの完全版はGitHub上に公開しています.

New permission model

Android Mでは新しいApp permission modelが提案されている.
従来モデルではアプリケーションが使用するパーミッションのリストをインストール時にユーザへ提示し, これらの使用許可を一度に取得する必要があった.
従来モデルでは一見不要と思えるパーミッションがリストされ, マルウェアと誤解されるケースもあり, アプリケーションのインストールを阻害する1つの障壁となっていた.

新しいApp permission modelの導入により, アプリはインストール時に必要最低限のパーミッションをユーザに求め, アプリケーションをインストール後, 必要となったタイミングでユーザにパーミッションの使用許可を求めることが可能になった.

アプリケーションの付加的な機能として提供されるものであればインストール時にパーミッションの許可を求めず, それを実行する時になって初めてパーミッションの使用許可を得れば良い.
たとえば, Google Analyticsでトラッキング情報の収集をユーザが求めないケースを考える.

従来モデルであれば, アプリケーションはGoogle Analyticsを使用するために下記のパーミッションの使用許可を得る必要があった. 例えユーザがGoogle Analyticsの使用に同意しなかったとしてもだ.

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

アプリケーションはGoogle Analytics以外でこれらのパーミッションが不要であったとしても, それを前もってユーザに理解してもらうのは困難だ. むしろ情報が抜き取られるのではないかという不信感をINTERNETパーミッションが与えているかもしれない.

新しいApp permission modelではアプリケーションのインストール時にこれらのパーミッションの使用許可を得る必要がなくなる.
代わりにそれらの機能を必要とする時, つまりアプリがインストールされた後, その機能をユーザが使用する際にパーミッションの使用許可を求める形に変えることができる.

ユーザがGoogle Analyticsの使用に同意しない場合(つまりINTERNETパーミッションの使用を拒否した場合), アプリケーションのインストールを諦める必要はない. 代わりにGoogle Analytics機能の使用のみを諦め, アプリケーションはそれ以外の機能を提供すれば良いのだ.

2015.06.25 訂正
INTERNETパーミッションはこちらに記載されている通り, PROTECTION_NORMALとなるため, Runtime Permissionのそれとは異なりインストール時に付与されるパーミッションです. 上の例は誤った内容であるためここに訂正します. (ご指摘いただいた@tao_gaku様ありがとうございました.)

対照的に, 許可を得られなければアプリケーションの価値を十分に提供できないようなパーミッションであればインストール時にまとめて取得しておいたほうが良いだろう. パーミッションの使用許可を得られなかったばかりにアプリケーションがまともに動作しないようであればユーザはあなたのアプリに低い評価をつけるかもしれない.

for M Preview Apps

M Preview向けに作成されたアプリ, つまりTargetSdkVersionがM Preview以上であるアプリは新しいApp Permission modelをベースに作成されているものとしてシステムは振る舞う.

Permission settings

M Previewではユーザがアプリケーションのパーミッションをいつでも剥奪できるようにパーミッションに関する設定画面を設けている.
Settings>Apps>Advanced>App permissionsからパーミッションの設定画面を起動できる.
アプリケーションは一度使用許可を得たパーミッションであっても, 次に実行するときにはそれが剥奪されている可能性を考慮しておく必要がある.

for System applications

通常のサイクルとは異なり, System applicationはインストール時にAndroid Manifestに定義されたパーミッションをユーザの確認なしにシステムによって許可される.
ただし, System applicationであっても例外なくユーザが設定アプリケーションから個別にPermissionを剥奪することが可能であることを忘れてはいけない.

for Signature protection

Protection levelがsignatureであるパーミッションは, 同パーミッションを持つアプリケーションが既に存在する場合, ユーザの確認なしにシステムによって許可される.
ただし, これについてもSystem applicationと同じく設定アプリケーションから個別にパーミッションの剥奪が可能であることに注意する必要がある.

for legacy applications

アプリケーションがM向けに作られたものではなく(targetSdkVerison < M), かつ新しいApp Permission Model上で動作する場合, こちらも同様に設定アプリケーションから個別にパーミッションを剥奪することが可能となっている.

ただし, M向けに作られたアプリケーションは新しいApp Permission Modelに対応していないことが予想されるため, システムはその場合に”アプリケーションが正常に動作しない可能性がある”旨のダイアログを表示してパーミッションの剥奪を行う.

Legacy app permission deny

レガシーなアプリケーションがパーミッションを剥奪された場合, パーミッションが必要なAPIを実行すると必ずセキュリティーパーミッションが発生するとは限らない. 代わりに空のデータが返却されることもあるだろうし, エラーを意味するコードが返されるかもしれないし, 予期しない動作となる場合もある.
例えば, カレンダー情報の検索に必要なクエリをパーミッションの使用許可なしで実行した場合, クエリの結果は”空”を返す.

for Communication

アプリケーションが写真を撮るケースにおいて, アプリケーションがandroid.permission.CAMERAのパーミッションをユーザにリクエストし, これの使用許可を 得た上でCamera APIを使用して写真を撮影する場合. このアプローチではアプリケーションにパーミッションの必要範囲が閉じており, 操作においても同様である.

異なって, Camera APIを使用せずACTION_IMAGE_CAPTURE Intentを使って写真の撮影を試みる場合, もしあなたのアプリケーションがACTION_IMAGE_CAPTURE Intentをサポートしている場合, さらにあなたのアプリケーションがまだandroid.permission.CAMERAのパーミッション使用許可を得ていない場合, あなたのアプリケーションはandroid.permission.CAMERAの使用許可をユーザに求めるが, この時に連携元のアプリケーションが持つパーミッションが適切かどうかの確認も怠ってはならない.

Coding!

Manifest

M向けに作成されるアプリは必ず新しいApp Permission Modelに対応しておく必要がある.
AndroidManifestにはアプリに必要なパーミッションを宣言しておき, 実行時に使用許可をとればよいパーミッションであれば必要になってから許可をとる仕組みを実装する.

M Previewで新しいPermission Modelをテストするにはbuild.gradleで次を設定する.

  • compileSdkVersion is set to ‘android-MNC’
  • minSdkVersion is set to ‘MNC’

Preview SDKでは古いプラットフォームでテストできないことも明示しておく.

  • targetSdkVersion is set to ‘MNC’

M Preview版では<uses-permission>を補完する要素として新たに<uses-permission-sdk-m>を宣言できる. <uses-permission-sdk-m>はMより古いOSを搭載した端末では無視され<uses-permission>の宣言がされていないものとして扱われる. そのため古いOS上ではアプリケーションをインストールする際に<uses-permission-sdk-m>で定義されたパーミッションの使用許可は表示されず, これの使用許可も与えられない.

パーミッションを必要とする場合, 古いOSでは<uses-permission>で宣言しておく必要がある. あなたのアプリケーションが古いOSで動作するのに必要なパーミッションは<uses-permission>で宣言しておく必要がある. ただし, M以降でのみ必要となるパーミッションは<uses-permission-sdk-m>で宣言する選択肢を得られる.

注意すべきは, <uses-permission>で宣言されたパーミッションであってもユーザはこれらのパーミッションを剥奪できるということである. M以降の端末をターゲットとするのであれば<uses-permission-sdk-m>, <uses-permission>双方で定義されたパーミッションが剥奪されている状況を考慮して実装しておく必要がある.

Check and Request Permissions

新しいApp Permission Modelに対応した端末であるかどうかを確認するにはBuild.VERSION.CODENAMEでOSのコードネームを確認する. M Previewである場合は"MNC"が返却される.

  private boolean isMNC() {
    // TODO: In the Android M Preview release, checking if the platform is M is done through the codename, 
    // not the version code.Once the API has been finalised, the following check
    // should be used:
    // return Build.VERSION.SDK_INT == Build.VERSION_CODES.MNC

    return "MNC".equals(Build.VERSION.CODENAME);
  }

アプリケーションがパーミッションの使用許可を得ているかどうかの確認にはContext.checkSelfPermission(permission_name)を使用する.
カメラの使用許可を確認するにはContext.checkSelfPermission(Manifest.permission.CAMERA)で, もし使用許可を持っていない場合はrequestPermissions()で許可を得る.

public void onClick(View v) {
  // 自アプリにカメラの使用許可があるか確認
  if (!hasSelfPermission(MainActivity.this, permission.CAMERA)) {
    // 使用許可がない場合はこれをリクエストする
    requestPermissions(new String[]{permission.CAMERA}, REQUEST_CODE);
  } else {
    // 使用許可がある場合はカメラを起動する
    launchCamera();
  }
}

private boolean hasSelfPermission(Activity activity, String permission) {
  return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}

requestPermissionsに指定するパーミッションはAndroidManifestであらかじめ<uses-permission-sdk-m>あるいは<uses-permission>で宣言されている必要がある. AndroidManifestで宣言されていないパーミッションをリクエストすることはできない.

requestPermissionsでパーミッションをリクエストするとシステムはパーミッションの使用許可をユーザにたずねるダイアログを表示する.

Allow Camera Permission

ここでの結果はActivity.onRequestPermissionsResultで得ることができる.

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  if (requestCode == REQUEST_CODE
      && permission.CAMERA.equals(permissions[0])
      && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    launchCamera();
  }
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Testing

新しいApp Permission Modelのために新たなADBコマンドがいくつか追加されている.

Install with permissions

adb installコマンドへ新たに-gオプションが追加された. この引数ありでインストールされたアプリケーションはAndroidManifestで宣言されている全てのパーミッションの使用が許可された状態でインストールされる.

$ adb install -g <path_to_apk>

Grant and revoke permissions

package manager (pm)コマンドにアプリケーションのパーミッションを付与/剥奪するコマンドが追加された.

# Permission Grant
$ adb pm grant <package_name> <grant_permission_name>

# Permission Revoke
$ adb pm revoke <package_name> <revoke_permission_name>

Best Practices

新しいApp Permission modelでは, あなたのアプリケーションがパーミッションを求める度にユーザの操作を中断する. これはユーザ体験を阻害する要因にもなるため, できる限りその頻度を抑える努力をすべきである.

例えば, あなたのアプリケーションでカメラで撮影した画像を取得するためにCAMERAパーミッションの使用許可を得てCamera APIを実装するのではなく, MediaStore.ACTION_IMAGE_CAPTURE のIntentによるアプリ連携でこれを実装することができる.

また, 一度に大量のパーミッション使用許可を求めるべきではない. パーミッションは必要となった時に求めるように努め, ユーザを唖然とさせないようにすること.
例えば, あなたがフォトグラフィアプリケーションを作成している場合, アプリケーションを起動した際にCameraパーミッションを求めるようにすべきだが, 撮影した画像をContactsデータを参照してShareする機能を有していた場合に, Contacts参照のパーミッション使用許可までアプリ起動時に取得してはならない.
後者のパーミッションはユーザのShareアクションまで待って, そこで初めてパーミッションの使用許可を得るのが常套手段である.

Many permission allowing

Explain why you need permissions

パーミッションの使用許可を得るダイアログには, あなたのアプリケーションがどのような理由でそのパーミッションの使用許可を求めているのかの説明文は表示されない. たとえばフォトグラフィアプリケーションが突然ローケーションサービスの使用許可を求めるとユーザはなぜフォトグラフィアプリケーションにロケーションサービスのパーミッションが必要であるのか困惑し不安になる. これは良いユーザ体験を生まない.

フォトグラフィアプリケーションは画像に埋め込まれたgeotagを解釈し, よりよいユーザ体験を提供することをユーザに説明する必要があるかもしれない. それはrequestPermissions()を呼び出す前に済ませておいたほうがよい.
例えばアプリケーションのチュートリアルにそれを埋め込んでおく方法がある. チュートリアルで機能を説明し, その時点でパーミッションの使用許可を取得しておく方法も有効だ. あるいはそこでShareのデモンストレーションをやってしまってもよいかもしれない. ユーザはあなたのアプリケーションが不必要にパーミッションの使用許可を求めていないことを理解し安心できるだろう.
もちろん, 全てのユーザが忠実にチュートリアルを読むとは限らず, また一度許可したとしても後からこれを剥奪される可能性も残っているため, Shareを実行する際にはパーミッションのチェックをいつも通り実施する必要はある.

以上.