はじめに
Marshmallow (Android 6.0) からアプリユーザーが
パーミッションの指定を細かく設定できるようになりました。
この対応に追われた開発者は多いはず…!と思ってググってみたら、いっぱいヒットしました。
ありがとう!そして、ありがとう!!
で、もうその辺いちいちコーディングするのめんどくさかったので、
一つのフラグメントにまとめてみましたメモ。
やりたいこと
- パーミッション要求周りの基底フラグメントを作る
パーミッションはどう変わったのか
おさらい的にパーミッションはどう変わったのかというと、
最初に書いちゃったけど、ユーザーがアプリのパーミッションを細かく設定できるようになりました。
パーミッションの設定画面はこんな感じ。
位置情報とか連絡帳とかSDカードへのアクセスとか、
そういう一部のデンジャラスなパーミッションがずらずらーっと並んで、
それぞれ「これは許可するけど、これはしーない!」みたいなことができます。
一部のデンジャラスなパーミッションって何?ってなったエンジニアは、ここを見ればいいと思います。
この変更、ユーザー側からするとちょっと安心な気分になるんだけど、
開発者からするとちょっと対応がめんどくさい。ちょっとちょっと。
ちょっとめんどくさい対応の流れ
開発者側が、このパーミッション設定に正しく対応しようとした場合、以下のような流れになります。
- パーミッションが許可されているかチェック
- なぜそのパーミッションが必要なのか画面を表示すべきかチェック
- なぜそのパーミッションが必要なのか画面を表示
- パーミッション許可設定ダイアログ(個数分)表示
順に解説していきますが、前提として、
この項目のサンプルコードは全て AppCompatActivity に書いているということを覚えておいてください。
パーミッションが許可されているかチェック
まずは、使いたいパーミッションが許可されているかチェックします。
コード的にはこんな感じ。
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 許可されてないよー
} else
// 許可されてるよー
}
checkSelfPermission メソッドにチェックさせたいパーミッションを設定して、お問い合わせ。
Android 6.0 未満の場合は、常に許可されている体で返ってきます。やったね!
なぜそのパーミッションが必要なのか画面を表示すべきかチェック
パーミッションの要求ダイアログは、初回はこんな感じで出てきます。
ここで拒否した後、もう一度確認することがあった場合、こんな感じになります。
ここで、「今後は確認しない」のチェックがされたかどうかを確認するのが、次の記述です。
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// 説明画面を出してほしいよー
} else {
// 説明画面は出さなくていいよー
}
この shouldShowRequestPermissionRational は、
それぞれ true / false を以下のように返します。
パターン | true / false |
---|---|
1度もパーミッション要求ダイアログを出していないとき | false |
2度目以降のパーミッション要求ダイアログを出すとき | true |
2度目以降のパーミッション要求ダイアログの「今後は確認しない」がチェックされた後 | false |
このなが〜い名前のメソッドですが、パーミッション要求ダイアログの表示制御っぽい仕様のわりに
なんだかメソッド名が一致しないので、一度は悩んだ人も多いのではないでしょうか。
というか、私がそうでした。
相当ググりまくった結果、
「パーミッション要求ダイアログの前には、必要な理由を説明する画面の表示がいる(推奨)」ってことを学習。
しかし、表に書いたけども
「一度もパーミッション要求ダイアログを出していないときは false で返ってくる」でパニックに…。
Mashmallow は メダパニ をとなえた!
akitaika_ は こんらんしている!!
んでまぁ、どういうことかと言いますと、
公式にパーミッション要求のベストプラクティスが書いてあるので、ちゃんと読んだ方が良いと思います。 (・ω<)
オレオレ解釈によると、以下のような流れを想定&推奨しているようです。
(《》はユーザーの操作を表しています)
- チュートリアルで説明画面(チュートリアル用)を表示
- 《「OK」など処理継続っぽい選択をする》
- パーミッション要求ダイアログ(初回)を表示
- 《「許可しない」を選択し、アプリ開始》
- 《パーミッションが必要な機能を利用しようとする》
- 説明画面(なぜそのパーミッションが必要なのか詳細なもの)を表示
- 《「OK」など処理継続っぽい選択をする》
- パーミッション要求ダイアログ(「今後は確認しない」のチェック付き)を表示
- 《チェックつけたり、許可の選択したり》
つまり、初回なんだから説明するでしょ?ってことで、
「一度もパーミッション要求ダイアログを出していないとき」は、false で返してくるっぽい。
この悟りをひらくまでにすごく時間がかかった…。
なぜそのパーミッションが必要なのか画面を表示
そんなこんなで、この処理は特に用意されているわけではなく、あくまでも公式が推奨している処理です。
「なんでカメラアプリなのに、位置情報知りたいの?怖い><」
といったユーザーの恐怖を和らげてあげるような説明画面を用意してあげてください。
パーミッション許可設定ダイアログ(個数分)表示
と、ここまで来てやっとパーミッションの許可を要求するダイアログを出す処理が書けるようになります。
ダイアログを出すのは、こんな感じです。
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
REQUEST_CODE_PLEASE_GRANT_PERMISSION);
今までのチェックでは、1つのメソッドで1つのパーミッションに対してしか行えないのですが、
なんと!ここでは、まとめてパーミッションを指定することができます! フゥー!!
結果の受け取りもまとめて複数処理できるのですが、
onActivityResult のノリで onRequestPermissionsResult というメソッドが呼ばれるので、
アクティビティかフラグメントでしか受け取れません。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PLEASE_GRANT_PERMISSION) {
// 許可されたかチェック
for (int i = 0; i < permissions.length; i++) {
final String permission = permissions[i];
final int grantResult = grantResults[i];
switch (permission) {
case Manifest.permission.ACCESS_FINE_LOCATION:
if (grantResult == PackageManager.PERMISSION_GRANTED) {
// 許可されたよー
} else {
// 許可されなかったよー
}
break;
default:
break;
}
}
}
}
指定したパーミッションのパーミッショングループが異なる場合は、
ダイアログが「1/2」のような表示になり、連続でユーザーに選択させた後、
onRequestPermissionsResult にそれぞれの結果が返ってきます。
ところが、同じパーミッショングループは勝手に1つのダイアログにまとめられます。
例えば、位置情報のパーミッションである「Manifest.permission.ACCESS_FINE_LOCATION」と
「Manifest.permission.ACCESS_COARSE_LOCATION」を指定してダイアログを出しても、
「位置情報へのアクセスを許可しますか?」のダイアログしか表示されません。
このまとめられたダイアログに対して、許可ないし拒否した場合は、
指定したパーミッションの個数分「許可した」「拒否された」が返ってくるようになっています。
上の位置情報の例で「許可する」を選択した場合だと、onRequestPermissionsResult の permissions に
「Manifest.permission.ACCESS_FINE_LOCATION」と
「Manifest.permission.ACCESS_COARSE_LOCATION」が入り、
grantResults に2つのパーミッション分の「PackageManager.PERMISSION_GRANTED」が入ってくる感じです。
このおまとめ仕様からなんとなく察せられる気がしますが、
実はパーミッショングループ内の1つのパーミッションが許可されれば、
同じグループのパーミッションも許可されます。
つまり、「Manifest.permission.ACCESS_FINE_LOCATION」だけを指定してパーミッションを要求し、
これが許可された場合、「Manifest.permission.ACCESS_COARSE_LOCATION」も許可されるということです。
あと他には、マニフェストファイルにパーミッションの記載漏れがあった場合に、
その記載していないパーミッションを指定してダイアログを呼び出したりすると、
ダイアログは表示されずに、強制的に拒否された結果が即座に返ってきます。
例外は投げられないので、気をつけましょう。
おさらいまとめ
ということで、パーミッション要求周りの処理は、全部つなげるとこんな感じになります。
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 許可されてないよー
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// 説明画面を出してほしいよー
// TODO:ここで「なぜそのパーミッションが必要なのか画面」を出す
// この画面側で処理継続的な選択をされた場合は、別途 ActivityCompat.requestPermissions をするべし
} else {
// 説明画面はもう出さなくていいよー
// パーミッションの許可要求ダイアログを表示
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
REQUEST_CODE_PLEASE_GRANT_PERMISSION);
}
} else {
// 許可されてるよー
}
なんだか、requestPermissions が不思議な位置にいますが、
「今後は確認しない」のチェックがされた後は、
requestPermissions を呼んでもパーミッション要求ダイアログが表示されないので、
たぶんそういうのを見越しての位置なんだと思います。
(公式のサンプルコードとほぼ同じものです)
ちなみに、パーミッションの許可と「今後は確認しない」のチェックは、
端末設定の「アプリ」からデータ消去すれば、リセットされます。
RequestPermissionFragment の誕生
ということを踏まえて作成したフラグメントがこちらになります。
public class RequestPermissionFragment extends Fragment {
private static final String TAG = "RequestPermissionF";
private static final int REQUEST_CODE_ASK_FOR_PERMISSION = 200;
private static final String SAVED_KEY_REQUEST_PERMISSIONS = "saved_key_request_permissions";
private String[] mRequestPermissions;
private boolean mRationaleCancelable;
public RequestPermissionFragment() {
// Required empty public constructor
}
/**
* 要求が必要なパーミッションを保持する
*
* @param permissions 要求するパーミッション
*/
private void setRequestPermissions(String[] permissions) {
mRequestPermissions = permissions;
}
/**
* 要求が必要なパーミッションを取得
*
* @return 要求が必要なパーミッション
*/
private String[] getRequestPermissions() {
return mRequestPermissions;
}
@Override
public void onSaveInstanceState(Bundle outState) {
// 要求しているパーミッションを保存
outState.putStringArray(SAVED_KEY_REQUEST_PERMISSIONS, mRequestPermissions);
}
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
// 保存していたパーミッションを再設定
setRequestPermissions(savedInstanceState.getStringArray(SAVED_KEY_REQUEST_PERMISSIONS));
}
}
/**
* すべてのパーミッションが許可されているか
*
* @param permissions チェックするパーミッション
* @return 許可されている/許可されていない
*/
protected boolean isAllowedAllPermissions(final String... permissions) {
String[] denyPermissions = getDenyPermissions(permissions);
return (denyPermissions == null);
}
/**
* パーミッションの要求ダイアログを表示する
*/
protected final void requestPermissions(final String... permissions) {
// 許可されていないパーミッションを取得
String[] denyPermissions = getDenyPermissions(permissions);
// 説明画面を表示すべきパーミッションを取得
String[] shouldShowRationalePermissions = getShouldShowRationalePermissions(
denyPermissions);
if (shouldShowRationalePermissions == null) {
// 要求するパーミッションを設定
setRequestPermissions(denyPermissions);
// パーミッションの要求ダイアログを表示
requestPermissions(getRequestPermissions(), REQUEST_CODE_ASK_FOR_PERMISSION);
} else {
// 要求するパーミッションを設定
setRequestPermissions(shouldShowRationalePermissions);
// 説明画面を表示
showRationaleFragment();
}
}
/**
* 許可されていないパーミッションを取得
*
* @param permissions 確認するパーミッション
* @return 許可されていないパーミッション
*/
protected final String[] getDenyPermissions(final String... permissions) {
// パーミッションが許可されているか
final List<String> denyPermissionList = new ArrayList<>();
for (String permission : permissions) {
if (permission != null && ContextCompat.checkSelfPermission(getContext(), permission)
!= PackageManager.PERMISSION_GRANTED) {
denyPermissionList.add(permission);
}
}
// すべて許可されている場合は、nullを返却
if (denyPermissionList.size() == 0) {
Log.i(TAG, "All permissions are allowed.");
return null;
}
return toStringArray(denyPermissionList);
}
/**
* 説明を必要としているパーミッションを取得
*
* @param permissions 確認するパーミッション
* @return 説明を必要としているパーミッション
*/
protected final String[] getShouldShowRationalePermissions(final String... permissions) {
// パーミッションに対する説明画面が必要か
final List<String> shouldRequestPermissionList = new ArrayList<>();
for (String denyPermission : permissions) {
if (denyPermission != null && shouldShowRequestPermissionRationale(denyPermission)) {
shouldRequestPermissionList.add(denyPermission);
}
}
// 説明が必要なパーミッションがない場合は、nullを返却
if (shouldRequestPermissionList.size() == 0) {
Log.i(TAG, "All permissions haven't request explanation.");
return null;
}
return toStringArray(shouldRequestPermissionList);
}
/**
* 説明ダイアログを表示
*/
private void showRationaleFragment() {
RationaleDialogFragment rationaleDialogFragment = onCreateRationaleDialogFragment();
Bundle arguments = rationaleDialogFragment.getArguments();
if (arguments == null) {
arguments = new Bundle();
}
arguments.putString(RationaleDialogFragment.ARGUMENT_KEY_TITLE,
createRationaleTitle());
arguments.putString(RationaleDialogFragment.ARGUMENT_KEY_MESSAGE,
createRationaleMessage());
arguments.putString(RationaleDialogFragment.ARGUMENT_KEY_POSITIVE,
createRationalePositive());
arguments.putString(RationaleDialogFragment.ARGUMENT_KEY_NEGATIVE,
createRationaleNegative());
rationaleDialogFragment.setArguments(arguments);
rationaleDialogFragment.show(getChildFragmentManager(), RationaleDialogFragment.TAG);
}
/**
* 説明ダイアログフラグメントの生成イベント
*
* @return 説明ダイアログフラグメント
*/
@NonNull
protected RationaleDialogFragment onCreateRationaleDialogFragment() {
return new RationaleDialogFragment();
}
/**
* 説明ダイアログのタイトルを生成
*
* @return 説明ダイアログのタイトル
*/
protected String createRationaleTitle() {
return null;
}
/**
* 説明ダイアログのメッセージを生成
*
* @return 説明ダイアログのメッセージ
*/
protected String createRationaleMessage() {
return null;
}
/**
* 説明ダイアログのポジティブボタンのラベルを生成
*
* @return 説明ダイアログのポジティブボタンのラベル
*/
protected String createRationalePositive() {
return null;
}
/**
* 説明ダイアログのネガティブボタンのラベルを生成
*
* @return 説明ダイアログのネガティブボタンのラベル
*/
protected String createRationaleNegative() {
return null;
}
/**
* 説明ダイアログでユーザーが継続をした時の処理
*/
protected void onNextRequestPermissions(final String[] requestPermissions) {
// パーミッション要求ダイアログを表示
requestPermissions(requestPermissions, REQUEST_CODE_ASK_FOR_PERMISSION);
}
/**
* 説明ダイアログでユーザーがスキップした時の処理
*/
protected void onSkipRequestPermissions(final String[] requestPermissions) {
// 後処理
onRequestPermissionsFinally();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode != REQUEST_CODE_ASK_FOR_PERMISSION) {
return;
}
// 許可されたかチェック
for (int i = 0; i < permissions.length; i++) {
final String permission = permissions[i];
final int grantResult = grantResults[i];
if (grantResult == PackageManager.PERMISSION_GRANTED) {
onAllowPermission(permission);
} else {
onDenyPermission(permission);
}
}
// 後処理
onRequestPermissionsFinally();
}
/**
* パーミッションが受け入れられた時の処理
*
* @param permission パーミッション
*/
protected void onAllowPermission(final String permission) {
}
/**
* パーミッションが拒否された時の処理
*
* @param permission パーミッション
*/
protected void onDenyPermission(final String permission) {
}
/**
* パーミッション要求の後処理
*/
protected void onRequestPermissionsFinally() {
// 保持していたパーミッションを破棄
setRequestPermissions(null);
}
/**
* 説明ダイアログのキャンセルは可能か
*
* @return 可能/不可能
*/
protected final boolean isRationaleCancelable() {
return mRationaleCancelable;
}
/**
* 説明ダイアログのキャンセルの可否を設定
*
* @param cancelable 許可する/許可しない
*/
protected final void setRationaleCancelable(boolean cancelable) {
mRationaleCancelable = cancelable;
}
/**
* リストから配列に変換
*
* @param stringList 文字列のリスト
* @return 文字列の配列
*/
private String[] toStringArray(final List<String> stringList) {
return stringList.toArray(new String[stringList.size()]);
}
/**
* 説明ダイアログ
*/
public static class RationaleDialogFragment extends DialogFragment {
public static final String ARGUMENT_KEY_TITLE = "argument_key_title";
public static final String ARGUMENT_KEY_MESSAGE = "argument_key_message";
public static final String ARGUMENT_KEY_POSITIVE = "argument_key_positive";
public static final String ARGUMENT_KEY_NEGATIVE = "argument_key_negative";
private static final String TAG = "RationaleDialogFragment";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RequestPermissionFragment parent = (RequestPermissionFragment) getParentFragment();
if (parent != null) {
setCancelable(parent.isRationaleCancelable());
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getContext())
.setTitle(FragmentUtil.getStringFromArguments(this, ARGUMENT_KEY_TITLE))
.setMessage(FragmentUtil.getStringFromArguments(this, ARGUMENT_KEY_MESSAGE))
.setPositiveButton(FragmentUtil.getStringFromArguments(this,
ARGUMENT_KEY_POSITIVE, "OK"),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
onClickPositive();
}
})
.setNegativeButton(FragmentUtil.getStringFromArguments(this,
ARGUMENT_KEY_NEGATIVE, "CANCEL"),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
onClickNegative();
}
})
.create();
}
/**
* ポジティブボタン押下時の処理
*/
protected void onClickPositive() {
RequestPermissionFragment parent = (RequestPermissionFragment) getParentFragment();
if (parent != null) {
parent.onNextRequestPermissions(parent.getRequestPermissions());
}
}
/**
* ネガティブボタン押下時の処理
*/
protected void onClickNegative() {
RequestPermissionFragment parent = (RequestPermissionFragment) getParentFragment();
if (parent != null) {
parent.onSkipRequestPermissions(parent.getRequestPermissions());
}
}
@Override
public void onCancel(DialogInterface dialog) {
// キャンセル時はスキップ時と同じ扱い
RequestPermissionFragment parent = (RequestPermissionFragment) getParentFragment();
if (parent != null) {
parent.onSkipRequestPermissions(parent.getRequestPermissions());
}
}
}
}
これの利用例はこんな感じ。
public class SampleFragment extends RequestPermissionFragment implements HelperCallbacks.FusedLocationCallback {
private static final String TAG = "SampleFragment";
FusedLocationHelper mFusedLocationHelper;
private static final String[] USE_PERMISSIONS;
static {
// FIXME:同じパーミッショングループはまとめられる
USE_PERMISSIONS = new String[] {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
// FIXME:一つでも同じパーミッショングループは許可される
// この場合は、Manifest.permission.ACCESS_COARSE_LOCATION も自動的に許可される
// USE_PERMISSIONS = new String[] {
// Manifest.permission.ACCESS_FINE_LOCATION
// };
// FIXME:違うパーミッショングループが混じってたら複数段階のダイアログになる
// USE_PERMISSIONS = new String[] {
// Manifest.permission.ACCESS_FINE_LOCATION,
// Manifest.permission.BODY_SENSORS
// };
// FIXME:マニフェストファイルに書いてないパーミッションは即拒否される
// USE_PERMISSIONS = new String[] {
// Manifest.permission.READ_PHONE_STATE
// };
}
// ~~~ 省略 ~~~
@Override
protected void onAllowPermission(String permission) {
Log.d(TAG, "Allowed permission = " + permission);
if (!mFusedLocationHelper.isConnected()) {
mFusedLocationHelper.connect();
}
}
@Override
protected void onDenyPermission(String permission) {
Log.d(TAG, "Denied permission = " + permission);
}
@OnClick({R.id.button_connect, R.id.button_disconnect})
public void onClick(View v) {
final int id = v.getId();
switch (id) {
case R.id.button_connect:
if (isAllowedAllPermissions(USE_PERMISSIONS)) {
// みんな許可されてたら、位置情報の取得を開始
mFusedLocationHelper.connect();
} else {
// 使いたいパーミッションをまとめてリクエスト
requestPermissions(USE_PERMISSIONS);
}
break;
case R.id.button_disconnect:
if (mFusedLocationHelper.isConnected()) {
mFusedLocationHelper.disconnect();
}
break;
default:
break;
}
}
@Override
protected String createRationaleTitle() {
return "位置情報の取得許可";
}
@Override
protected String createRationaleMessage() {
return "このアプリでは位置情報を利用します。\n利用許可は次の画面で行います。";
}
@Override
protected String createRationalePositive() {
return "次へ";
}
@Override
protected String createRationaleNegative() {
return "スキップ";
}
// ~~~ 省略 ~~~
}
さりげなく Butter Knife していますが、そこは置いといて…。
RequestPermissionFragment が利用しているフラグメントは、android.support.v4.app.Fragment の方です。
すでにパーミッション要求のいろいろなメソッドが用意されているので、そのまんま利用しています。
FragmentUtil は自作クラスです。
getStringFromArguments メソッドは以下のようなメソッドです。
/**
* アーギュメントから文字列を取得
*
* @param fragment フラグメント
* @param key キー
* @param defaultValue デフォルト値
* @return 文字列
*/
public static String getStringFromArguments(final Fragment fragment, final String key,
final String... defaultValue) {
Bundle arguments = fragment.getArguments();
if (arguments == null) {
return (defaultValue.length != 0) ? defaultValue[0] : null;
} else {
return arguments.getString(key);
}
}
ロジックとかは、仕様について先にいろいろ解説しちゃったし、
わりと見たままの処理なので、特に解説はしないです。 (・ω<)☆
正直、ダイアログダイアログ&ダイアログな感じで UX 的には最悪だと思うので、
RationaleDialogFragment はもっと普通のフラグメントになっているといいと思います。
参考程度に眺めてください。
ちなみに、FusedLocationHelper も自作クラスです。
Fused Location Provider を簡単に扱いたくて作ったヤツです。
まだちょっと冗長な感じなので、完成してから記事にしますん。
追記
以下の観点から、ちょっとコードを変更しました。
- 説明ダイアログのキャンセル設定
当初は、「拒否できないダイアログはゴミ」くらいの思想でキャンセルできるようにしていたのですが、
よくよく考えたら、説明ダイアログの表示フラグはこちらからいじれず、
次のパーミッション要求ダイアログまでいってもらわないと2度と表示しないということはできないので、
キャンセル設定自体は可変にしつつ、デフォルトはキャンセル不可にしました。くそぅ。
- 説明ダイアログの改修しやすさ向上(?)
「説明ダイアログのキャンセル設定」の変更によって、
むしろ「次へ」ボタンだけでもよくなったことを加味して、
もうちょっと自由な感じで拡張できそうな雰囲気にしました。
具体的には、任意のアーギュメントも説明ダイアログに渡せるようにしました。
- キャンセル=スキップ
キャンセルとスキップって、結局期待する動作は同じだなと思って、同じ処理にしました。
変えたい場合は、拡張すればいいしね。
余談
連絡先パーミッション、マニフェストに定義してなくてもアプリの設定画面の一覧に出てくるのはなぜなのか…。
誤解を生みそうで嫌だから、表示しないようにできるならしたいところ。
すんごくググってもそれっぽい情報が見つけられなかったので、
知っている方がいたら、教えて欲しいです。
まとめ
- 仕様まで書いたら、前置きがすんごく長くなったよ!
- shouldShowRequestPermissionRationale は理解がまだちょっと怪しいかも?な感じだよ!
- ソースコードは使ってもいいけど、問題起きても責任は取らないよ!
参考
- Working with System Permissions
http://developer.android.com/intl/ja/training/permissions/index.html - Fun with permissions: Why the change in Android 6.0 may make you repeat yourself
http://www.androidcentral.com/run-permissions-why-change-android-60-may-make-you-repeat-yourself - Android M - check runtime permission - how to determine if the user checked “Never ask again”?
http://stackoverflow.com/questions/30719047/android-m-check-runtime-permission-how-to-determine-if-the-user-checked-nev