|
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(); |
|
} |
|
} |
|
} |
|
} |