Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid writing duplicate boilerplate code for requesting permissions?

I'm updating my app to be Android 6-compatible. The permission model is not very complex in theory, but now I'm in the process of implementing it and I find myself writing the same ugly boilerplate code in every one of my activities that require permissions.

For every permission I need, there is a

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.PERMISSION) !=
                PackageManager.PERMISSION_GRANTED) {
} else {
}

and then in the onRequestPermissionsResult I have to check/filter the results of each request and translate that to something my activity understands.

I'm updating my second activity now, and the permission code is so similar to the first that it almost looks like it's copy-pasted. The lines are long, the code is similar, and it simply looks ugly.

I don't want to use a third-party solution, I have tried some, but I would prefer to have full control over the code. For instance, some libraries do not support Java 8, which I am using in my project.

What can I do to avoid a bunch of duplicate code in all of my activities?

like image 818
Tim Avatar asked Aug 22 '16 12:08

Tim


2 Answers

I didn't want to use any of the available libraries for the reasons explained in the question, so I developed something myself.

All my activities that require one or more permissions inherit from a PermissionActivity that handles all the permission related tasks.

How it works is your activity calls

if (checkHasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { }

from the parent class. If the permission is already granted, the code can continue. If not, the parent class will request the permission and send the results to the child class using an abstract method and/or one or multiple overridable methods.


The parent class

This class can be left unchanged, apart from the switch blocks in messageForRationale() and requestCodeForPermission(). Update those for the permissions your app needs.

/**
 * An activity that can be extended to simplify handling permissions.
 * <p>
 * Deriving classes will not have to write boilerplate code and code duplication between activities
 * that share this functionality is avoided.
 */
public abstract class PermissionActivity extends AppCompatActivity {

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // If multiple permissions were requested in one call, check if they were all granted.
        if (requestCode == RequestCode.PERMISSION_MULTIPLE) {
            boolean allPermissionsGranted = true;
            for (int grantResult : grantResults) {
                if (grantResult != PackageManager.PERMISSION_GRANTED) {
                    allPermissionsGranted = false;
                }
            }

            if (allPermissionsGranted) {
                onAllPermissionsGranted(permissions);
                return;
            }
        }

        // Else, check each one if it was granted/denied/blocked.
        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                // User granted permission.
                onPermissionGranted(permissions[i]);
            } else {
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
                    // User denied permission.
                    onPermissionDenied(permissions[i]);
                } else {
                    // User denied permission and checked 'never ask again'.
                    onPermissionBlocked(permissions[i]);
                }
            }
        }
    }

    /**
     * Checks if the app has the given permission(s).
     * <p>
     * If not, it will request them.
     * <p>
     * The method is called `checkHasPermission` to avoid the linter showing a warning in the
     * child class when it's delegating permission checks to its parent class. See
     * http://stackoverflow.com/questions/36031218/check-android-permissions-in-a
     * -method/36193309#36193309 for details.
     */
    public boolean checkHasPermission(int requestCode, String... permissions) {
        if (!(permissions.length > 0)) {
            throw new IllegalArgumentException("must request at least one permission");
        }

        if (requestCode == RequestCode.PERMISSION_MULTIPLE) {
            List<String> permissions_ = new ArrayList<>();

            for (String permission : permissions) {
                if (ActivityCompat.checkSelfPermission(this, permission) !=
                        PackageManager.PERMISSION_GRANTED) {
                    permissions_.add(permission);
                }
            }

            if (!permissions_.isEmpty()) {
                requestPermissions(this, permissions_.toArray(new String[permissions_.size()]), requestCode);
                return false;
            } else {
                return true;
            }
        } else {
            if (ActivityCompat.checkSelfPermission(this, permissions[0]) !=
                    PackageManager.PERMISSION_GRANTED) {
                requestPermissions(this, permissions, requestCode);
                return false;
            } else {
                return true;
            }
        }
    }

    /**
     * Requests the given permissions.
     */
    private void requestPermissions(Activity activity, String permissions[], int resultCode) {
        showRequestPermissionsDialog(activity, permissions, resultCode);
    }

    /**
     * Called when a rationale (explanation why a permission is needed) should be shown to the user.
     * <p>
     * If the user clicks the positive button, the permission is requested again, otherwise the
     * dialog is dismissed.
     */
    public void showRationaleDialog(Activity activity, String permission, String message,
                                    int resultCode) {
        new AlertDialog.Builder(activity)
                .setMessage(message)
                .setPositiveButton("ok", (dialog, which) ->
                        showRequestPermissionDialog(activity, permission, resultCode))
                .setNegativeButton("not now", (dialog, which) -> { /* Do nothing */ })
                .show();
    }

    /**
     * Requests a single permission.
     */
    private void showRequestPermissionDialog(Activity activity, String permission, int resultCode) {
        ActivityCompat.requestPermissions(activity, new String[]{permission}, resultCode);
    }

    /**
     * Requests multiple permissions in one call.
     */
    private void showRequestPermissionsDialog(Activity activity, String[] permissions,
                                              int resultCode) {
        ActivityCompat.requestPermissions(activity, permissions, resultCode);
    }

    /**
     * Returns a message to be shown to the user that explains why a specific permission is
     * required.
     */
    public String messageForRationale(String permission) {
        String s;
        switch (permission) {
            case Manifest.permission.READ_PHONE_STATE:
                s = "access this device's state";
                break;
            case Manifest.permission.ACCESS_FINE_LOCATION:
                s = "access the location of this device";
                break;
            case Manifest.permission.SEND_SMS:
                s = "send text messages";
                break;
            default:
                throw new IllegalArgumentException("Permission not handled: " + permission);
        }

        return String.format("MyApp needs permission to %s.", s);
    }

    /**
     * Get the RequestCode for the given permission.
     */
    public int requestCodeForPermission(String permission) {
        int code;
        switch (permission) {
            case Manifest.permission.READ_PHONE_STATE:
                code = RequestCode.PERMISSION_READ_PHONE_STATE;
                break;
            case Manifest.permission.ACCESS_FINE_LOCATION:
                code = RequestCode.PERMISSION_FINE_LOCATION;
                break;
            case Manifest.permission.SEND_SMS:
                code = RequestCode.PERMISSION_SEND_SMS;
                break;
            // TODO: add required permissions for your app
            default:
                throw new IllegalArgumentException("Permission not handled: " + permission);
        }

        return code;
    }

    /**
     * Called if all requested permissions were granted in the same dialog.
     * E.g. FINE_LOCATION and SEND_SMS were requested, and both were granted.
     * <p>
     * Child class can override this method if it wants to know when this happens.
     * <p>
     * Linter can show an unjust "call requires permission" warning in child class if a method that
     * requires permission(s) is called. Silence it with `@SuppressWarnings("MissingPermission")`.
     */
    protected void onAllPermissionsGranted(String[] permissions) {

    }

    /**
     * Called for all permissions that were granted in the same dialog, in case not all were
     * granted. E.g. if FINE_LOCATION, COARSE_LOCATION and SEND_SMS were requested and FINE_LOCATION
     * was not granted but COARSE_LOCATION and SEND_SMS were, it will be called for COARSE_LOCATION
     * and SEND_SMS.
     * <p>
     * Child class can override this method if it wants to know when this happens.
     * <p>
     * Linter can show an unjust "call requires permission" warning in child class if a method that
     * requires permission(s) is called. Silence it with `@SuppressWarnings("MissingPermission")`.
     */
    protected void onPermissionGranted(String permission) {

    }

    /**
     * Called for all permissions that were denied in the same dialog, handled one by one.
     * <p>
     * Child class should not override this general behavior.
     */
    protected void onPermissionDenied(String permission) {
        String message = messageForRationale(permission);
        showRationaleDialog(this, permission, message, requestCodeForPermission(permission));
    }

    /**
     * Called for all permissions that were blocked in the same dialog, handled one by one.
     * <p>
     * Blocked means a user denied a permission with the 'never ask again' checkbox checked.
     * <p>
     * Child class must override and decide what to do when a permission is blocked.
     */
    protected abstract void onPermissionBlocked(String permission);
}

The -> notation are lambda expressions.

RequestCode is an interface solely used for abstracting away the numbers:

public interface RequestCode {
    int PERMISSION_READ_PHONE_STATE = 0;
    int PERMISSION_FINE_LOCATION = 1;
    int PERMISSION_SEND_SMS = 2;
    int PERMISSION_MULTIPLE = 3;
}

you can change it as you like. Don't use numbers over 256. If you do, an exception will be thrown saying

You can only use the lower 8 bits for the request code.


In activities where permission is required, you can use it like this (just an example). Be sure to have the activity extend PermissionActivity

private void callThisSomewhere() {
    if (checkHasPermission(RequestCode.PERMISSION_READ_PHONE_STATE,
            Manifest.permission.READ_PHONE_STATE)) {
        tryDoStuffWithPhoneState();
    }
}

@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void doStuffWithPhoneState() {
    // Do stuff.
}

@Override
public void onPermissionGranted(String permission) {
    tryDoStuffWithPhoneState();
}

@Override
public void onPermissionBlocked(String permission) {
    // Disable parts of app that require this permission.
}

If you are requesting multiple permissions in one go, you should use RequestCode.PERMISSION_MULTIPLE. Otherwise only the first permission will be requested.

Permissions that are not listed in AndroidManifest.xml will be automatically blocked without showing the user the dialog, so be sure to add any permissions you have to request in the manifest as well.


Limitations

This solution is not compatible with fragments or services.

like image 99
Tim Avatar answered Oct 20 '22 18:10

Tim


This is what I used for managing permissions in my app.

Parent Class for Permission

    public class PermissionManager extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
    private static final int REQUEST_CODE = 200;
    private Activity context;

    private String[] permissions;
    private ArrayList<String> grantedPermissions = new ArrayList<>();
    private RequestedPermissionResultCallBack callBack;

    @Override
    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
    }

    @Override
    protected void onStart() {
        super.onStart();
        setContentView(R.layout.actvity_permission);
        checkForRequiredPermission(getIntent().getStringArrayExtra(getString(R.string.permission)));
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        switch (requestCode) {

            case REQUEST_CODE:

                checkForGrantedPermissions(permissions, grantResults);
                break;
        }

    }


    @TargetApi(Build.VERSION_CODES.M)
    private void checkForRequiredPermission(String[] permissions) {
        ArrayList<String> requiredPermissionList = new ArrayList<>();
        for (String permission : permissions) {
            if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                requiredPermissionList.add(permission);
            } else {
                grantedPermissions.add(permission);
            }
        }

        if (requiredPermissionList.size() > 0) {
            (this).requestPermissions(requiredPermissionList.toArray(new String[requiredPermissionList.size()]), REQUEST_CODE);
        } else {

            setResult(grantedPermissions);
        }

    }


    public void checkForGrantedPermissions(String[] permissions, int[] grantResults) {

        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                grantedPermissions.add(permissions[i]);
            }

        }
        setResult(grantedPermissions);

    }

    private void setResult(ArrayList<String> grantedPermissions) {
        Intent intent = new Intent();
        intent.putStringArrayListExtra(getString(R.string.granted_permission), grantedPermissions);
        setResult(Activity.RESULT_OK, intent);
        this.finish();
    }
}

When you want to check for permission call this class like this

private void checkForPermissions() {
        Intent intent = new Intent(this, PermissionManager.class);
        intent.putExtra(getString(R.string.permission), permission);
        startActivityForResult(intent, AppConstants.PERMSION_REQUEST_CODE);
    }

Here permission is an array of permission you want to ask for something like this

    private String permission[] = new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS};

and this is the code for onActivityResult

case AppConstants.PERMSION_REQUEST_CODE:
                    ArrayList<String> grantedPermissionList = data.getStringArrayListExtra(getString(R.string.granted_permission));

                    if (grantedPermissionList != null && grantedPermissionList.size() > 0 && grantedPermissionList.contains(permission[0])) {
                        createRequest();

                    } else {
                        showSettingsDialog(getString(R.string.permission_required));
                    }

                    break;
like image 36
Vivek Mishra Avatar answered Oct 20 '22 19:10

Vivek Mishra