Skip to content

Instantly share code, notes, and snippets.

@namannik
Last active June 29, 2018 00:02
Show Gist options
  • Save namannik/e1aa2282005aca4410aa9f91f6aa5853 to your computer and use it in GitHub Desktop.
Save namannik/e1aa2282005aca4410aa9f91f6aa5853 to your computer and use it in GitHub Desktop.
Helper class to manage Android permissions

PermissionsChecker

This class simplifes permissions requests on Android M (API level 23) and newer by handling various conditions that affect what type of permission prompt will be shown.

Installation

  1. Add PermissionsChecker.java (from this Gist) to your Android project.
  2. Add the following style to your project's styles.xml:
<resources>
    ...
    <style name="Theme.Transparent" parent="android:Theme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:backgroundDimEnabled">false</item>
    </style>
   ...
</resources>
  1. Add the following Activity to your AndroidManifest.xml:
    <application
        ...
        <activity android:name=".PermissionsChecker$PermissionsCheckerActivity"
            android:theme="@style/Theme.Transparent"/>
        ...
    </application>

Usage

// Create a PermissionsChecker.
PermissionsChecker permissionsChecker = new PermissionsChecker(context);

// Define which permissions you want to check.
// (These may be in different permission groups.)
String[] permissions = {Manifest.permission.CALL_PHONE, Manifest.permission.READ_CONTACTS};

// Check permissions.
if (permissionsChecker.appHasPermissions(permissions)) {
    // do something that requires permissions
} else {
    // Request permissions:
    permissionsChecker.requestPermissions(
            activity,    /* The parent activity. */
            permissions, /* An array of permissions to prompt the user for. */
            null,        /* An optional message to explain why this permission is needed. */
            true,        /* Whether or not to allow the user to cancel the permissions request.
                            Only set this to false if your app absolutely can not run without this
                            permission. */
            permissionsCompletionHandler /*The RequestCompletionHandler that will be called when permission
                                           is approved or denied.  May be null.*/
    );
}

PermissionsChecker.RequestCompletionHandler permissionsCompletionHandler =
    new PermissionsChecker.RequestCompletionHandler() {
        @Override
        public void onRequestPermissionsResult(@NonNull String[] permissions, @NonNull int[] grantResults) {
            // permissions: The requested permissions.
            // grant results: The results for the corresponding permissions, which is either PERMISSION_GRANTED or PERMISSION_DENIED.
            int i = 0;
            for (String permission : permissions) {
                // check permissions and do something that requires permissions if permission was granted
            }
        }
    };
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.app.AlertDialog;
import android.text.Html;
import android.text.Spanned;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
@SuppressWarnings("unused")
@RequiresApi(api = 23)
public class PermissionsChecker {
private Context mContext;
@Nullable static RequestCompletionHandler mCompletionHandler;
private AlertDialog mPermissionInstructionsDialog;
PermissionsChecker(Context context) {
this.mContext = context.getApplicationContext();
}
public interface RequestCompletionHandler {
/**
* Callback for the result from permissions requested using requestPermissions.
* @param permissions The requested permissions.
* @param grantResults The results for the corresponding permissions, which is either
* PERMISSION_GRANTED or PERMISSION_DENIED.
*/
void onRequestPermissionsResult(@NonNull String[] permissions, @NonNull int[] grantResults);
}
/**
* Check whether the app has been granted particular permissions.
* This method will throw a java.lang.SecurityException if any of the permission are not in the
* Android manifest.
* @param permissions An array of permissions to check, as defined in Manifest.permission.
* @return Returns true if all requested permissions have been granted. Returns false if any
* permission is denied.
*/
public boolean appHasPermissions(String[] permissions) {
throwIfPermissionsNotInManifest(permissions);
for (String permission : permissions)
if (mContext.getApplicationContext().checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED)
return false;
return true;
}
/**
* The requestPermissions method can handle permissions from multiple permission groups.
* However, it may be advantageous to request only one permission group at a time. In that
* situation, this method can be used to determine which permissions are in which groups.
* These groups can then be passed to requestPermission so that only one permission
* group is requested at a time.
*
* See https://developer.android.com/guide/topics/permissions/overview#perm-groups for more
* information about permission groups.
*
* @param permissions An array of permissions.
* @return An array of arrays of permissions, grouped by items in the same permissions group.
*/
public String[][] permissionsByGroup(String[] permissions) {
HashSet<String> groups = new HashSet<>();
for (String permission : permissions)
groups.add(permissionsGroup(mContext, permission));
String[][] groupsArray = new String[groups.size()][];
int i = 0;
for (String group : groups) {
groupsArray[i] = filterPermissionsByGroups(mContext, permissions, new String[]{ group });
i++;
}
return groupsArray;
}
/**
* Prompt the user to accept permissions with dangerous protection level. No prompt is shown
* for permissions already granted to the app and permissions that are not dangerous. This
* method will throw a java.lang.SecurityException if any of the permission are not in the
* Android manifest.
*
* @param activity The parent activity.
* @param permissions An array of permissions to prompt the user for, as defined in
* Manifest.permission.
* @param justificationMessage An optional message to explain why this permission is needed.
* May be null.
* @param allowCancel Whether or not to allow the user to cancel the permissions request.
* Only set this to false if your app absolutely can not run without this
* permission.
* @param completionHandler The RequestCompletionHandler that will be called when permission
* is approved or denied.
*/
public void requestPermissions(Activity activity,
String[] permissions,
@Nullable String justificationMessage,
boolean allowCancel,
@Nullable RequestCompletionHandler completionHandler) {
mCompletionHandler = completionHandler;
ArrayList<String> noPermissionsArray = new ArrayList<>();
HashSet<String> permissionGroups = new HashSet<>();
for (String permission : permissions) {
if (!appHasPermissions(new String[]{permission})) {
noPermissionsArray.add(permission);
String group = permissionsGroup(mContext, permission);
permissionGroups.add(group);
}
}
String[] requiredPermissions = noPermissionsArray.toArray(new String[0]);
if (requiredPermissions.length == 0) return;
throwIfPermissionsNotInManifest(requiredPermissions);
// If, when previously prompted for permission, the user had checked the "Don't ask again"
// box, calling requestPermissions again does nothing, which would appear to the user as if
// the current request is being ignored. At this point, the only way to enable the
// permission is for the user to turn on the permission from within the device's settings.
//
// When "Don't ask again" had previously been checked, the
// shouldShowRequestPermissionRationale method returns false. This flag is used to
// determine whether the user should be instructed to enable the app's permissions in the
// device's settings.
//
// Additionally, if the user has never been prompted for the permission,
// shouldShowRequestPermissionRationale returns false. Therefore, a counter is stored in
// the app's preferences that to determine if this is the first time the user has been
// prompted.
//
// Request 1: Show justification (if desired)
// Request 2: Prompt for permission
// Request 3+: Use shouldShowRequestPermissionRationale to see if instructions should be shown.
HashSet<String> groupsRequiringInstructions = new HashSet<>();
for (String group : permissionGroups) {
String requestCountKey = group + ".requestCount";
SharedPreferences preferenceManager = PreferenceManager.getDefaultSharedPreferences(mContext);
int requestCount = preferenceManager.getInt(requestCountKey, 0);
requestCount++;
preferenceManager.edit().putInt(requestCountKey, requestCount).apply();
if (requestCount == 1 && justificationMessage != null) {
showPermissionsJustification(activity, requiredPermissions,
justificationMessage, allowCancel);
break;
} else {
String instructionsGroup = null;
if (requestCount >= 3) {
String[] groupPermissions = filterPermissionsByGroups(mContext, requiredPermissions, new String[]{group});
for (String permission : groupPermissions) {
if (!activity.shouldShowRequestPermissionRationale(permission)) {
instructionsGroup = group;
break;
}
}
}
if (instructionsGroup != null)
groupsRequiringInstructions.add(instructionsGroup);
else
requestPermissions(permissions);
}
}
if (groupsRequiringInstructions.size() > 0) {
int size = groupsRequiringInstructions.size();
String[] groups = groupsRequiringInstructions.toArray(new String[size]);
showPermissionsInstructions(activity, groups, justificationMessage, allowCancel);
}
}
private void showPermissionsJustification(final Activity activity, final String[] permissions,
String message, boolean allowCancel) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Light_Dialog);
builder.setMessage(message);
builder.setCancelable(false);
if (allowCancel)
builder.setNegativeButton("Don't allow", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int whichButton) {
int[] grantResults = new int[permissions.length];
int i = 0;
for (String permission : permissions) {
int result = mContext.getApplicationContext().checkSelfPermission(permission);
grantResults[i] = result;
i++;
}
if (mCompletionHandler != null)
mCompletionHandler.onRequestPermissionsResult(permissions, grantResults);
}
});
builder.setPositiveButton("Continue", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int whichButton) {
requestPermissions(permissions);
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
@SuppressLint("ObsoleteSdkInt")
private void showPermissionsInstructions(final Activity activity, final String[] permissionsGroups,
@Nullable String justificationMessage, boolean allowCancel) {
if (mPermissionInstructionsDialog != null)
mPermissionInstructionsDialog.dismiss();
PackageManager packageManager = mContext.getPackageManager();
ArrayList<String> groupNames = new ArrayList<>();
for (String group : permissionsGroups) {
try {
PermissionGroupInfo groupInfo = packageManager.getPermissionGroupInfo(group, 0);
groupNames.add(groupInfo.loadLabel(packageManager).toString());
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
Collections.sort(groupNames);
StringBuilder permissionsGroupsText = new StringBuilder();
for (int i = 0; i < groupNames.size(); i++) {
if (i > 0 && i == groupNames.size() - 1) {
if (groupNames.size() > 2)
permissionsGroupsText.append(", and ");
else
permissionsGroupsText.append(" and ");
} else if (i > 0 && permissionsGroups.length > 2) {
permissionsGroupsText.append(", ");
}
String groupName = groupNames.get(i);
permissionsGroupsText.append("<b>").append(groupName).append("</b>");
}
String singleOrPlural;
if (permissionsGroups.length == 1)
singleOrPlural = " permission";
else
singleOrPlural = " permissions";
if (justificationMessage == null)
justificationMessage = "This requires the " + permissionsGroupsText.toString() + " " + singleOrPlural + " to be enabled.";
String instructions =
justificationMessage + "<br>" +
"<br>" +
"1. Tap <b>Open Settings</b> below.<br>" +
"<br>" +
"2. Tap <b>Permissions</b>.<br>" +
"<br>" +
"3. Set " + permissionsGroupsText.toString() + " to ON.";
AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Light_Dialog);
Spanned strMessage;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N)
strMessage = Html.fromHtml(instructions, Html.FROM_HTML_MODE_LEGACY);
else
//noinspection deprecation
strMessage = Html.fromHtml(instructions);
builder.setMessage(strMessage);
if (allowCancel)
builder.setNegativeButton(R.string.cancel, null);
else
builder.setCancelable(false);
builder.setPositiveButton("Open Settings", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Intent waitingActivityIntent = new Intent(mContext, PermissionsCheckerActivity.class);
waitingActivityIntent.putExtra("pendingPermissionsGroups", permissionsGroups);
waitingActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(waitingActivityIntent);
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", mContext.getPackageName(), null);
intent.setData(uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
});
mPermissionInstructionsDialog = builder.create();
mPermissionInstructionsDialog.show();
}
@Nullable
private static String[] groupPermissionsInManifest(Context context, String[] groups) {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
String[] allPermissions = packageInfo.requestedPermissions;
return filterPermissionsByGroups(context, allPermissions, groups);
}
private boolean permissionIsInManifest(String permission) {
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
String[] requestedPermissions = packageInfo.requestedPermissions;
for (String requestedPermission : requestedPermissions)
if (requestedPermission.equalsIgnoreCase(permission))
return true;
return false;
}
private void throwIfPermissionsNotInManifest(String[] permissions) {
ArrayList<String> permissionsNotInManifest = new ArrayList<>();
for (String permission : permissions)
if (!permissionIsInManifest(permission))
permissionsNotInManifest.add(permission);
if (permissionsNotInManifest.size() > 0) {
String message;
if (permissionsNotInManifest.size() == 1) {
message = "Permission for " + permissionsNotInManifest.get(0) + " must be declared in the Android manifest.";
} else {
message = "The following permissions must be declared in the Android manifest:";
for (String permission : permissionsNotInManifest)
//noinspection StringConcatenationInLoop
message = message + "\n" + permission;
}
throw new java.lang.SecurityException(message);
}
}
@Nullable
private static String permissionsGroup(Context context, String permission) {
PackageManager packageManager = context.getPackageManager();
try {
PermissionInfo permissionInfo = packageManager.getPermissionInfo(permission, 0);
return permissionInfo.group;
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
private static String[] filterPermissionsByGroups(Context context, String[] permissions, String[] groups) {
ArrayList<String> filteredPermissions = new ArrayList<>();
for (String permission : permissions) {
for (String group : groups) {
String thisPermissionGroup = permissionsGroup(context, permission);
if (thisPermissionGroup != null && thisPermissionGroup.equalsIgnoreCase(group))
filteredPermissions.add(permission);
}
}
return filteredPermissions.toArray(new String[0]);
}
private static int[] grantResults(Context context, String[] permissions) {
int[] grantResults = new int[permissions.length];
int i = 0;
for (String permission : permissions) {
int result = context.getApplicationContext().checkSelfPermission(permission);
grantResults[i] = result;
i++;
}
return grantResults;
}
private void requestPermissions(String[] permissions) {
Intent intent = new Intent(mContext, PermissionsCheckerActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("permissions", permissions);
mContext.startActivity(intent);
}
public static class PermissionsCheckerActivity extends Activity {
String[] pendingPermissionsGroups;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] permissions = getIntent().getStringArrayExtra("permissions");
pendingPermissionsGroups = getIntent().getStringArrayExtra("pendingPermissionsGroups");
if (permissions != null)
requestPermissions(permissions, 0);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (PermissionsChecker.mCompletionHandler != null)
//noinspection ConstantConditions
PermissionsChecker.mCompletionHandler.onRequestPermissionsResult(permissions, grantResults);
finish();
}
@Override
protected void onResume() {
super.onResume();
if (pendingPermissionsGroups != null) {
String[] permissions = groupPermissionsInManifest(this, pendingPermissionsGroups);
if (permissions != null) {
int[] grantResult = grantResults(this, permissions);
if (PermissionsChecker.mCompletionHandler != null)
Objects.requireNonNull(PermissionsChecker.mCompletionHandler).onRequestPermissionsResult(permissions, grantResult);
}
finish();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment