Android6.0 (API23) Permission を何とかするコード

Introduction

Marshmallow からのDangerous Permissionsの扱いが凶悪なのでいろいろと参考に、書いてみました。
というか、パクリ。というかニコイチみたいな。

Overview

どう動くかというと、
文字で書こうとしたけど、めんどくさいのでUTSLじゃなくて、動画とりましたよ。と。
動画はサンプルアプリではなく、個人用に作った懐中電灯アプリです。
youtu.be
『今後は確認しない』があるので、それを選ばれちゃったときは、端末の設定画面で許可をしてもらうべく説明を出し、intentで飛ばします。(動画にはないけど、『アプリ情報』を押せば設定のアプリのところへ飛べます。)

参考元は一番下に列挙

実は、書いてしばらくたってから記事にしてるのでソース読まないと忘れてます。のでコピペで動くかは知らんす。
UTSL(Use the Source, Luke)てやつですすいません。
ご意見ご指摘いただくとむせび泣いて喜びます。

Source

//HelperClass そのままつかう

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.KeyEvent;

public abstract class PermissionHelper {

	private final int REQUEST_CODE;
	private final String PERMISSION;
	private int deniedCount;

	/**
	 * コンストラクタ
	 *
	 * @param permission    扱うパーミッション(例:Manifest.permission.WRITE_EXTERNAL_STORAGE)
	 * @param requestCode   内部的に使用するリクエストコード(重複しない値を選ぶこと)
	 */
	public PermissionHelper(@NonNull String permission, @IntRange(from = 0, to = 255) int requestCode) {
		PERMISSION = permission;
		REQUEST_CODE = requestCode;
		deniedCount = 0;
	}

	public void tryExecute(@NonNull final Activity activity) {
		if (ContextCompat.checkSelfPermission(activity, PERMISSION) != PackageManager.PERMISSION_GRANTED) {
			if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, PERMISSION)) {
				//  許諾要求
				requestPermission(activity);
			}
		} else {
			// 許諾されているので、やりたいことをやる
			onAllowed();
		}
	}

	/**
	 *
	 * @param activity
	 * @param message
	 * @param ok
	 * @param deny
	 */
	public void tryExecute(@NonNull final Activity activity, int message, int ok, int deny){
		tryExecute(activity, activity.getString(message), activity.getString(ok), activity.getString(deny));
	}
	/**
	 * 対象のパーミッションが許諾されていれば、本来やりたいことを実行し、そうでない場合は許諾を得るメソッド
	 *
	 * @param activity  Build.VERSION.SDK_INT < 23 の場合は、OnRequestPermissionsResultCallbackをimplementsしたActivity
	 * @param message   このパーミッションが必要な理由を説明する文字列(nullの場合、説明するダイアログは省略)→NonNull
	 * @param ok   説明するダイアログを閉じるボタンのキャプション(nullの場合は、"OK"を表示)→NonNull
	 */
	public void tryExecute(@NonNull final Activity activity, @NonNull String message, @NonNull String ok, @NonNull final String deny) {
		if (ContextCompat.checkSelfPermission(activity, PERMISSION) != PackageManager.PERMISSION_GRANTED) {
			// 以前に許諾して、今後表示しないとしていた場合は、ここにはこない
			if (ActivityCompat.shouldShowRequestPermissionRationale(activity, PERMISSION)) {
				// ユーザに許諾してもらうために、なんで必要なのかを説明する
				// SnackBarを使うのもいいかもしんないけど、許諾要求自体がダイアログで出てくるので、それに合わせた。
				AlertDialog.Builder builder = new AlertDialog.Builder(activity);
				builder.setMessage(message);
				builder.setPositiveButton(ok, new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						//  許諾要求
						requestPermission(activity);
					}
				});
				builder.setNegativeButton(deny, new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface dialogInterface, int i){
						onExecuteDenied();
					}
				});
				builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
					@Override
					public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
						// Disable Back key and Search key
						switch (keyCode) {
						case KeyEvent.KEYCODE_BACK:
						case KeyEvent.KEYCODE_SEARCH:
							return true;
						default:
							return false;
						}
					}
				});
				AlertDialog alertDialog = builder.create();
				alertDialog.setCanceledOnTouchOutside(false);
				alertDialog.show();
			} else {
				//  許諾要求
				requestPermission(activity);
			}
		} else {
			// 許諾されているので、やりたいことをやる
			onAllowed();
		}
	}

	/**
	 * 許諾要求をだすメソッド
	 *
	 * @param activity  Build.VERSION.SDK_INT < 23 の場合は、OnRequestPermissionsResultCallbackをimplementsしたActivity
	 */
	private void requestPermission(@NonNull Activity activity) {
		String[] permissions = new String[] { PERMISSION };
		ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE);
	}

	/**
	 * OnRequestPermissionsResultCallback#onRequestPermissionsResult()で呼び出すメソッド
	 * 渡されたパラメータをそのまま、このメソッドに渡す
	 *
	 * @param requestCode   リクエストコード
	 * @param permissions   パーミッションの配列
	 * @param grantResults  許諾状態の配列
	 */
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
		if (requestCode == REQUEST_CODE) {
			if (permissions.length > 0 && permissions[0].equals(PERMISSION) &&
					grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
				// 許諾されたので、やりたいことをやる
				onAllowed();
			} else {
				onFinallyDenied();
			}
		}
	}

	public void onRequestPermissionsResult(final Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults,
										   int message, int goSetting, int cancel, int requestCodeForSetting) {
		onRequestPermissionsResult(activity, requestCode, permissions, grantResults,
				activity.getString(message), activity.getString(goSetting), activity.getString(cancel), requestCodeForSetting);
	}

	/**
	 * OnRequestPermissionsResultCallback#onRequestPermissionsResult()で呼び出すメソッド
	 * 渡されたパラメータをそのまま、このメソッドに渡す
	 * Never ask again 対応 message,goSetting,cancelはその文章
	 * Never ask again でdeny した場合、ダイアログを出しセッティングへ飛ばせる
	 * また、
	 * shouldShowRequestPermissionRationaleのへんてこ動作対応に、denyされた1度目だけもう一度説明から許可画面を出す
	 * @param requestCode   リクエストコード
	 * @param permissions   パーミッションの配列
	 * @param grantResults  許諾状態の配列
	 */
	public void onRequestPermissionsResult(final Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults,
										   final String message, final String goSetting, final String cancel, final int requestCodeForSetting) {
		if (requestCode == REQUEST_CODE) {
			if (permissions.length > 0 && permissions[0].equals(PERMISSION) &&
					grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
				// 許諾されたので、やりたいことをやる
				onAllowed();
			} else {
				// when "never ask again" checked
				if (!shouldShowRequestPermissionRationale(activity, PERMISSION)) {
					goSettingDialog(activity, message, goSetting, cancel, requestCodeForSetting);
				}else{
					if(deniedCount < 1){
						onFirstDenied();
					}else{
						onFinallyDenied();
					}
					deniedCount++;
				}
			}
		}
	}

	/**
	 * 本当にやりたいことは、これをOverrideして実装する
	 */
	protected abstract void onAllowed();

	/**
	 * 権限を許諾されなかったときの処理が必要であれば実装する
	 */
	protected void onFinallyDenied(){}

	protected void onFirstDenied(){}

	protected void onExecuteDenied(){}

	protected void onGoSettingDenied(){}

	/**
	 * @param activity
	 * @param permission
	 * @return
	 */
	private boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			return activity.shouldShowRequestPermissionRationale(permission);
		}
		return true;
	}

	private void goSettingDialog(final Activity activity, String message, String goSettings, String finallyDeny, final int requestCodeForSetting){
		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
		builder.setMessage(message);
		builder.setPositiveButton(goSettings, new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
				// システムのアプリ設定画面
				Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + activity.getPackageName()));
				activity.startActivityForResult(intent, requestCodeForSetting);
			}
		});
		builder.setNegativeButton(finallyDeny, new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialogInterface, int i){
				onGoSettingDenied();
			}
		});
		builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
			@Override
			public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
				// Disable Back key and Search key
				switch (keyCode) {
				case KeyEvent.KEYCODE_BACK:
				case KeyEvent.KEYCODE_SEARCH:
					return true;
				default:
					return false;
				}
			}
		});
		AlertDialog alertDialog = builder.create();
		alertDialog.setCanceledOnTouchOutside(false);
		alertDialog.show();
	}

}

で、使うには、Activity とかに以下のようにして使う。

public class MainActivity extends Activity implements ActivityCompat.OnRequestPermissionsResultCallback {

	@Override
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);

		setContentView(R.layout.main);

		tryToGetPermission();
	}

	/**
	 * for Marshmallow
	 */
	private void tryToGetPermission(){
		helper.tryExecute(this, R.string.marshmallow_execute_message, R.string.ok, R.string.quit_app);
	}

	private static final int RC_MARSHMALLOW = 4218;		//onActivityResultでつかうrequestCodeなので他と被らないように
	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
		helper.onRequestPermissionsResult(this, requestCode, permissions, grantResults,
				R.string.marshmallow_setting_message, R.string.marshmallow_setting, R.string.quit_app, RC_MARSHMALLOW);
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data){
		super.onActivityResult(requestCode, resultCode, data);
		if(requestCode == RC_MARSHMALLOW){
			tryToGetPermission();
		}
	}
	// PermissionHelper自体は、Activityの状態に依存しないのでいつでも生成できる
	// ここでは、外部ストレージへの書込権限を扱うように指定している
	// 123は内部的に使うユニーク値なので、適当に与える(ここでしか使わないので、即値でもOK)
	private MarshmallowHelper helper = new MarshmallowHelper(Manifest.permission.WRITE_EXTERNAL_STORAGE, 123);

	// 無名クラスで実装してもいいけど、ここでは名前付きの内部クラスとして実装している
	// 単に、見やすさを優先しただけ
	private class MarshmallowHelper extends PermissionHelper {
		/**
		 * コンストラクタ
		 *
		 * @param permission  扱うパーミッション(例:Manifest.permission.WRITE_EXTERNAL_STORAGE)
		 * @param requestCode 内部的に使用するリクエストコード(重複しない値を選ぶこと)
		 */
		private MarshmallowHelper(@NonNull String permission, @IntRange(from = 0, to = 255) int requestCode){
			super(permission, requestCode);
		}
		@Override
		protected void onAllowed() {
			//実際にやりたい処理
			onCreateMain();
		}
		@Override
		protected void onFirstDenied(){
			tryToGetPermission();
		}
		@Override
		protected void onExecuteDenied(){
			finishApp();
		}
		@Override
		protected void onGoSettingDenied(){
			finishApp();
		}
		@Override
		protected void onFinallyDenied(){
			finishApp();
		}
		private void finishApp(){
			finish();
		}
	}

	private void onCreateMain(){
		//続行する処理を書く
	}
}