* Important: If your activity subclasses {@link PermisoActivity}, this is already handled for you. * @param activity The activity that is currently active. */ public void setActivity(@NonNull Activity activity) { mActivity = new WeakReference<>(activity); } /** * Request one or more permissions from the system. Make sure that you are either subclassing {@link PermisoActivity} * or that you have set your current activity using {@link Permiso#setActivity(Activity)}! * @param callback * A callback that will be triggered when the results of your permission request are available. * @param permissions * A list of permission constants that you are requesting. Use constants from * {@link android.Manifest.permission}. */ @MainThread public void requestPermissions(@NonNull IOnPermissionResult callback, String... permissions) { Activity activity = checkActivity(); final RequestData requestData = new RequestData(callback, permissions); // Mark any permissions that are already granted for (String permission : permissions) { if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) { requestData.resultSet.grantPermissions(permission); } } // If we had all of them, yay! No need to do anything else. if (requestData.resultSet.areAllPermissionsGranted()) { requestData.onResultListener.onPermissionResult(requestData.resultSet); } else { // If we have some unsatisfied ones, let's first see if they can be satisfied by an active request. If it // can, we'll re-wire the callback of the active request to also trigger this new one. boolean linkedToExisting = linkToExistingRequestIfPossible(requestData); // If there was no existing request that can satisfy this one, then let's make a new permission request to // the system if (!linkedToExisting) { // Mark the request as active final int requestCode = markRequestAsActive(requestData); // First check if there's any permissions for which we need to provide a rationale for using String[] permissionsThatNeedRationale = requestData.resultSet.getPermissionsThatNeedRationale(activity); // If there are some that need a rationale, show that rationale, then continue with the request if (permissionsThatNeedRationale.length > 0) { requestData.onResultListener.onRationaleRequested(new IOnRationaleProvided() { @Override public void onRationaleProvided() { makePermissionRequest(requestCode, requestData); } }, permissionsThatNeedRationale); } else { makePermissionRequest(requestCode, requestData); } } } } /** * This method needs to be called by your activity's {@link Activity#onRequestPermissionsResult(int, String[], int[])}. * Simply forward the results of that method here. *
* Important: If your activity subclasses {@link PermisoActivity}, this is already handled for you.
* @param requestCode
* The request code given to you by {@link Activity#onRequestPermissionsResult(int, String[], int[])}.
* @param permissions
* The permissions given to you by {@link Activity#onRequestPermissionsResult(int, String[], int[])}.
* @param grantResults
* The grant results given to you by {@link Activity#onRequestPermissionsResult(int, String[], int[])}.
*/
@MainThread
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
Activity activity = checkActivity();
if (mCodesToRequests.containsKey(requestCode)) {
RequestData requestData = mCodesToRequests.get(requestCode);
requestData.resultSet.parsePermissionResults(permissions, grantResults, activity);
requestData.onResultListener.onPermissionResult(requestData.resultSet);
mCodesToRequests.remove(requestCode);
} else {
Log.w(TAG, "onRequestPermissionResult() was given an unrecognized request code.");
}
}
/**
* A helper to show your rationale in a {@link android.app.DialogFragment} when implementing
* {@link IOnRationaleProvided#onRationaleProvided()}. Automatically invokes the rationale callback when the user
* dismisses the dialog.
* @param title
* The title of the dialog. If null, there will be no title.
* @param message
* The message displayed in the dialog.
* @param buttonText
* The text you want the dismissal button to show. If null, defaults to {@link android.R.string#ok}.
* @param rationaleCallback
* The callback to be trigger
*/
@MainThread
public void showRationaleInDialog(
@Nullable String title,
@NonNull String message,
@Nullable String buttonText,
@NonNull final IOnRationaleProvided rationaleCallback) {
PermisoDialogFragment.Builder builder = new PermisoDialogFragment.Builder()
.setTitle(title)
.setMessage(message)
.setButtonText(buttonText);
showRationaleInDialog(builder, rationaleCallback);
}
/**
* A helper to show your rationale in a {@link android.app.DialogFragment} when implementing
* {@link IOnRationaleProvided#onRationaleProvided()}. Automatically invokes the rationale callback when the user
* dismisses the dialog.
* @param builder
* A reference to PermisoDialogFragment.Builder containing parameters for displaying the Dialog.
* @param rationaleCallback
* The callback to be trigger
*/
@MainThread
public void showRationaleInDialog(
final PermisoDialogFragment.Builder builder,
final IOnRationaleProvided rationaleCallback) {
Activity activity = checkActivity();
FragmentManager fm = activity.getFragmentManager();
PermisoDialogFragment dialogFragment = (PermisoDialogFragment) fm.findFragmentByTag(PermisoDialogFragment.TAG);
if (dialogFragment != null) {
dialogFragment.dismiss();
}
dialogFragment = builder.build(activity);
// We show the rationale after the dialog is closed. We use setRetainInstance(true) in the dialog to ensure that
// it retains the listener after an app rotation.
dialogFragment.setOnCloseListener(new PermisoDialogFragment.IOnCloseListener() {
@Override
public void onClose() {
rationaleCallback.onRationaleProvided();
}
});
dialogFragment.show(fm, PermisoDialogFragment.TAG);
}
// =====================================================================
// Private
// =====================================================================
/**
* Checks to see if there are any active requests that are already requesting a superset of the permissions this
* new request is asking for. If so, this will wire up this new request's callback to be triggered when the
* existing request is completed and return true. Otherwise, this does nothing and returns false.
* @param newRequest The new request that is about to be made.
* @return True if a request was linked, otherwise false.
*/
private boolean linkToExistingRequestIfPossible(final RequestData newRequest) {
boolean found = false;
// Go through all outstanding requests
for (final RequestData activeRequest : mCodesToRequests.values()) {
// If we find one that can satisfy all of the new request's permissions, we re-wire the active one's
// callback to also call this new one's callback
if (activeRequest.resultSet.containsAllUngrantedPermissions(newRequest.resultSet)) {
final IOnPermissionResult originalOnResultListener = activeRequest.onResultListener;
activeRequest.onResultListener = new IOnPermissionResult() {
@Override
public void onPermissionResult(ResultSet resultSet) {
// First, call the active one's callback. It was added before this new one.
originalOnResultListener.onPermissionResult(resultSet);
// Next, copy over the results to the new one's resultSet
String[] unsatisfied = newRequest.resultSet.getUngrantedPermissions();
for (String permission : unsatisfied) {
newRequest.resultSet.requestResults.put(permission, resultSet.requestResults.get(permission));
}
// Finally, trigger the new one's callback
newRequest.onResultListener.onPermissionResult(newRequest.resultSet);
}
@Override
public void onRationaleRequested(IOnRationaleProvided callback, String... permissions) {
activeRequest.onResultListener.onRationaleRequested(callback, permissions);
}
};
found = true;
break;
}
}
return found;
}
/**
* Puts the RequestData in the map of requests and gives back the request code.
* @return The request code generated for this request.
*/
private int markRequestAsActive(RequestData requestData) {
int requestCode = mActiveRequestCode++;
mCodesToRequests.put(requestCode, requestData);
return requestCode;
}
/**
* Makes the permission request for the request that matches the provided request code.
* @param requestCode The request code of the request you want to run.
* @param requestData The {@link RequestData} representing the request you want to run.
*/
private void makePermissionRequest(int requestCode, RequestData requestData) {
Activity activity = checkActivity();
ActivityCompat.requestPermissions(activity, requestData.resultSet.getUngrantedPermissions(), requestCode);
}
/**
* Ensures that our WeakReference to the Activity is still valid. If it isn't, throw an exception saying that the
* Activity needs to be set.
*/
private Activity checkActivity() {
Activity activity = mActivity.get();
if (activity == null) {
throw new IllegalStateException("No activity set. Either subclass PermisoActivity or call Permiso.setActivity() in onCreate() and onResume() of your Activity.");
}
return activity;
}
// =====================================================================
// Inner Classes
// =====================================================================
/**
* A callback interface for receiving the results of a permission request.
*/
public interface IOnPermissionResult {
/**
* Invoked when the results of your permission request are ready.
* @param resultSet An object holding the result of your permission request.
*/
void onPermissionResult(ResultSet resultSet);
/**
* Called when the system recommends that you provide a rationale for a permission. This typically happens when
* a user denies a permission, but they you request it again.
* @param callback A callback to be triggered when you are finished showing the user the rationale.
* @param permissions The list of permissions for which the system recommends you provide a rationale.
*/
void onRationaleRequested(IOnRationaleProvided callback, String... permissions);
}
/**
* Simple callback to let Permiso know that you have finished providing the user a rationale for a set of permissions.
* For easy handling of this callback, consider using
* {@link Permiso#showRationaleInDialog(String, String, String, IOnRationaleProvided)}.
*/
public interface IOnRationaleProvided {
/**
* Invoke this method when you are done providing a rationale to the user in
* {@link IOnPermissionResult#onRationaleRequested(IOnRationaleProvided, String...)}. The permission request
* will not be made until this method is invoked.
*/
void onRationaleProvided();
}
private static class RequestData {
IOnPermissionResult onResultListener;
ResultSet resultSet;
public RequestData(@NonNull IOnPermissionResult onResultListener, String... permissions) {
this.onResultListener = onResultListener;
resultSet = new ResultSet(permissions);
}
}
/**
* A class representing the results of a permission request.
*/
public static class ResultSet {
private Map
*
*/
public class PermisoActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Permiso.getInstance().setActivity(this);
}
@Override
protected void onResume() {
super.onResume();
Permiso.getInstance().setActivity(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permiso.getInstance().onRequestPermissionResult(requestCode, permissions, grantResults);
}
}
================================================
FILE: permiso/src/main/java/com/greysonparrelli/permiso/PermisoDialogFragment.java
================================================
package com.greysonparrelli.permiso;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.app.AlertDialog;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
/**
* A DialogFragment created for convenience to show a simple message explaining why a certain permission is being
* requested and trigger a listener when it has been closed. Intended to be used by
* {@link Permiso#showRationaleInDialog(String, String, String, Permiso.IOnRationaleProvided)}.
*/
public class PermisoDialogFragment extends DialogFragment {
public static final String TAG = "PermisoDialogFragment";
private static final String KEY_TITLE = "title";
private static final String KEY_MESSAGE = "message";
private static final String KEY_BUTTON_TEXT = "button_text";
private static final String KEY_HAS_HTML = "has_html";
private static final String KEY_THEME_ID = "theme_id";
private String mTitle;
private String mMessage;
private String mButtonText;
private boolean mHasHtml;
private int mThemeId;
private IOnCloseListener mOnCloseListener;
/**
* Creates a new {@link PermisoDialogFragment}. Only intended to be used by
* {@link Permiso#showRationaleInDialog(String, String, String, Permiso.IOnRationaleProvided)}.
* @param title The title of the dialog. If null, no title will be displayed.
* @param message The message to be shown in the dialog.
* @param buttonText The text to label the dialog button. If null, defaults to {@link android.R.string#ok}.
* @return A new {@link PermisoDialogFragment}.
*/
public static PermisoDialogFragment newInstance(
@Nullable String title,
@NonNull String message,
@Nullable String buttonText) {
return newInstance(new Builder()
.setTitle(title)
.setMessage(message)
.setButtonText(buttonText));
}
private static PermisoDialogFragment newInstance(@NonNull Builder builder) {
PermisoDialogFragment dialogFragment = new PermisoDialogFragment();
// Build arguments bundle
Bundle args = new Bundle();
args.putBoolean(KEY_HAS_HTML, builder.isHtml());
args.putString(KEY_TITLE, builder.getTitle());
args.putString(KEY_MESSAGE, builder.getMessage());
args.putString(KEY_BUTTON_TEXT, builder.getButtonText());
args.putInt(KEY_THEME_ID, builder.getThemeId());
dialogFragment.setArguments(args);
return dialogFragment ;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain instance state so we can keep our listeners registered after a rotation
setRetainInstance(true);
mTitle = getArguments().getString(KEY_TITLE);
mMessage = getArguments().getString(KEY_MESSAGE);
mButtonText = getArguments().getString(KEY_BUTTON_TEXT);
mHasHtml = getArguments().getBoolean(KEY_HAS_HTML);
mThemeId = getArguments().getInt(KEY_THEME_ID);
}
@Override
public void onDestroyView() {
// If we don't do this, the DialogFragment is not recreated after a rotation. See bug:
// https://code.google.com/p/android/issues/detail?id=17423
if (getDialog() != null && getRetainInstance()) {
getDialog().setDismissMessage(null);
}
super.onDestroyView();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), mThemeId);
// Title
if (mTitle != null) {
builder.setTitle(mTitle);
}
// Message
if (mHasHtml) {
TextView msg = new TextView(getActivity());
msg.setText(Html.fromHtml(mMessage));
msg.setMovementMethod(LinkMovementMethod.getInstance());
builder.setView(msg);
} else if (mMessage != null) {
builder.setMessage(mMessage);
}
// Button text
String buttonText;
if (mButtonText != null) {
buttonText = mButtonText;
} else {
buttonText = getString(android.R.string.ok);
}
builder.setPositiveButton(buttonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mOnCloseListener != null) {
mOnCloseListener.onClose();
}
}
});
return builder.create();
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
if (mOnCloseListener != null) {
mOnCloseListener.onClose();
}
}
/**
* Sets the listener that will be triggered when this dialog is closed by a user action. This includes clicking
* the dismissal button as well as clicking in the area outside of the dialog. NOT triggered by rotation.
* @param listener Listener you want triggered when this dialog is closed by a user action.
*/
public void setOnCloseListener(IOnCloseListener listener) {
mOnCloseListener = listener;
}
/**
* A simple listener that will be triggered when this dialog is closed by a user action.
*/
public interface IOnCloseListener {
/**
* Called when the dialog is closed by a user action. This includes clicking the dismissal button as well as
* clicking in the area outside of the dialog. NOT triggered by rotation.
*/
void onClose();
}
/**
* Build a {@link PermisoDialogFragment}. Gives the opportunity to set strings using either a raw string or a
* resource id.
*/
public static class Builder {
private int titleId = 0;
private String title = null;
private int buttonTextId = 0;
private String message = null;
private int messageId = 0;
private boolean interpretHtml = false;
private String buttonText = null;
private int themeId = 0;
public Builder() {}
public Builder(int titleId, int msgBody, int buttonTextId) {
this.titleId = titleId;
this.messageId = msgBody;
this.buttonTextId = buttonTextId;
}
// =====================================================================
// Getters
// =====================================================================
public String getTitle() {
return title;
}
public String getMessage() {
return message;
}
public String getButtonText() {
return buttonText;
}
public boolean isHtml() {
return interpretHtml;
}
public int getThemeId() {
return themeId;
}
// =====================================================================
// Setters
// =====================================================================
/**
* Set the title that will appear at the top of the dialog.
*/
public Builder setTitle(@StringRes int resId) {
titleId = resId;
return this;
}
/**
* Set the title that will appear at the top of the dialog.
*/
public Builder setTitle(String val) {
title = val;
return this;
}
/**
* Set the message that will appear in the body of the dialog.
*/
public Builder setMessage(@StringRes int resId) {
messageId = resId;
return this;
}
/**
* Set the message that will appear in the body of the dialog.
*/
public Builder setMessage(String val) {
message = val;
return this;
}
/**
* Set the text that will appear as dismissal button in the corner of the dialog.
*/
public Builder setButtonText(@StringRes int resId) {
buttonTextId = resId;
return this;
}
/**
* Set the text that will appear as dismissal button in the corner of the dialog.
*/
public Builder setButtonText(String string) {
buttonText = string;
return this;
}
/**
* Set whether or not you want the message text to be interpreted as HTML.
*/
public Builder setHtmlInterpretation(boolean interpretHtml) {
this.interpretHtml = interpretHtml;
return this;
}
/**
* Set theme id that the {@link AlertDialog} will use.
*/
public Builder setThemeId(int themeId) {
this.themeId = themeId;
return this;
}
public PermisoDialogFragment build(Context context) {
if (titleId > 0) { title = context.getString(titleId); }
if (messageId > 0) { message = context.getString(messageId); }
if (buttonTextId > 0) { buttonText = context.getString(buttonTextId); }
return PermisoDialogFragment.newInstance(this);
}
}
}
================================================
FILE: permiso/src/main/res/values/strings.xml
================================================