https://en.proft.me/2017/06/14/runtime-permissions-android-marshmallow-60-and-abo/


Introduction

Before API 23 the requested permissions are presented to the user before installing the application. The user needs to decide if these permissions shall be given to the application.

If the user denies a required permission, the related application cannot be installed. The check of the permission is only performed during installation. Permissions cannot be denied or granted after the installation.

Android 6.0 Marshmallow (API 23) introduced a new runtime permission model. There are a number of libraries available to make runtime permissions easier. If you to get started quickly, check out guide on managing runtime permissions with PermissionsDispatcher.

android_marshmallow_permissions.png

This concept is beneficial because users can stay informed about each and every permission which he/she grants to the application. User can revoke the granted permission through Settings > Apps > APPNAME > Permissions from his/her device.

If you don’t implement runtime permission, then your application will crash or will not work properly on the device having MarshMallow and above.

There are two important protection levels:

  • Normal permissions are permissions which are deemed harmless for the users privacy or the operation of other applications. For example, the permission to set the time zone or Internet access. Normal permission are automatically granted to the application. See Normal permissions for a complete list.
  • Dangerous permissions affect the users private information, or could potentially affect his data or the operation of other application. For example, the ability to read the users contact data. Dangerous permissions must be granted by the user at runtime to the app.

Both types of permissions are necessary to be defined in AndroidManifest.xml but only dangerous permissions require a runtime request.

Open AndroidManifest.xml and add the following permissions:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

You can check, if you have the permission, via the checkSelfPermission method.

int CALLBACK_NUMBER = 1;
String wantPermission = Manifest.permission.ACCESS_COARSE_LOCATION;
// called in a standard activity, use ContextCompat.checkSelfPermission for AppCompActivity
int permissionCheck = checkSelfPermission(this, wantPermission);
if (!permissionCheck == PackageManager.PERMISSION_GRANTED) {
    // user may have declined earlier, ask Android if we should show him a reason
    if (shouldShowRequestPermissionRationale(MainActivity.this, wantPermission)) {
        // show an explanation to the user
    } else {
        // request the permission.
        // CALLBACK_NUMBER is a integer constants
        requestPermissions(MainActivity.this,  new String[]{wantPermission}, CALLBACK_NUMBER);
    }
} else {
    // got permission, use it
}

Each permission request needs three parameters. The first is context, the second a String array of permission(s), and the third the requestCode of type Integer. The last parameter is a random code attached to the request, and can be any number that suits your use case. When a result returns in the activity, it contains this code and uses it to differentiate multiple results from each other.

There are one interesting thing that dangerous permissions are grouped into the Permission Group, as displayed in the table. It means that you need to request for only one permission from group. If any permission from permission group is granted, then other permissions from the same group will be granted automatically. Likewise if any permission from permission group is denied, then entire group will be denied. For example, once READ_EXTERNAL_STORAGE is granted, application will also grant WRITE_EXTERNAL_STORAGEpermission.

Permission GroupPermissions
android.permission-group.CALENDAR
  • android.permission.READ_CALENDAR
  • android.permission.WRITE_CALENDAR
android.permission-group.CAMERA
  • android.permission.CAMERA
android.permission-group.CONTACTS
  • android.permission.READ_CONTACTS
  • android.permission.WRITE_CONTACTS
  • android.permission.GET_ACCOUNTS
android.permission-group.LOCATION
  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.ACCESS_COARSE_LOCATION
android.permission-group.MICROPHONE
  • android.permission.RECORD_AUDIO
android.permission-group.PHONE
  • android.permission.READ_PHONE_STATE
  • android.permission.CALL_PHONE
  • android.permission.READ_CALL_LOG
  • android.permission.WRITE_CALL_LOG com.
  • android.voicemail.permission.ADD_VOICEMAIL
  • android.permission.USE_SIP
  • android.permission.PROCESS_OUTGOING_CALLS
android.permission-group.SENSORS
  • android.permission.BODY_SENSORS
android.permission-group.SMS
  • android.permission.SEND_SMS
  • android.permission.RECEIVE_SMS
  • android.permission.READ_SMS
  • android.permission.RECEIVE_WAP_PUSH
  • android.permission.RECEIVE_MMS
  • android.permission.READ_CELL_BROADCASTS
android.permission-group.STORAGE
  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE

So, in Marshmallow and above these dangerous permissions should be granted at run time and the normal permissions are granted automatically. For Lollipop (API level 22) or lower android versions the permissions specified in manifest are granted at installation.

Let’s create a sample application with Android Studio to get clear knowledge of how these permissions work. Don't forget to select the target SDK version as Marshmallow (API level 23).

Add the following permission to AndroidManifest.xml.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

The above added permission is a dangerous permission which we will request at run time.

The activity_main.xml layout has a parent RelativeLayout with two child Button, one to check permission and other to request permission. The complete activity_main.xml layout is below.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btnCheckPermission"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Check permission"/>

    <Button
        android:id="@+id/btnRequestPermission"
        android:layout_below="@+id/btnCheckPermission"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Request permission"/>
</RelativeLayout>

Our Activity extends to AppCompatActivity and implements OnClickListener interface. Our app has two buttons. One is to check whether the permission is already granted or not. Another is used to request permission if permission is not granted.

The permission is checked by calling ContextCompat.checkSelfPermission() method and the result is verified with PackageManager.PERMISSION_GRANTED. Here I display a Snackbar to show whether the permission is granted or not. Snackbar is a new view which is displayed at the bottom similar to Toast and also we can add action button to Snackbar.

The permission is requested using ActivityCompat.requestPermissions() method by passing the Activity, permissions as String array and request code. And the result is handled using the onRequestPermissionsResult() method. The request code is verified and the result is diplayed in Snackbarwhether permission is granted or denied.

If the user once denied the permission you cannot request permission again. In the second launch, user will get a "Never ask again" option to prevent application from asking this permission in the future. If this option is checked before denying. The user need to grant you permisiion explicitly in App Settings. Next time we call requestPermissions, this dialog will not be appeared for this kind of permission anymore. Instead, it just does nothing.

For that you can explain to the user why that permission is required. It can be checked using ActivityCompat.shouldShowRequestPermissionRationale() method by passing Activity and the permission. If it returns true the user have denied the permission and you can request the user to allow the permission or you can disable that paricular function.

The permission check process is wrapped in checkPermission() method. The request permission process is wrapped in requestPermission() method.

The complete MainActivity class is below.

import android.Manifest;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Context context;
    private Activity activity;
    private static final int PERMISSION_REQUEST_CODE = 1;
    String wantPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_permissions);

        context = this;
        activity = MainActivity.this;

        Button btnCheckPermission = (Button)findViewById(R.id.btnCheckPermission);
        Button btnRequestPermission = (Button)findViewById(R.id.btnRequestPermission);
        btnCheckPermission.setOnClickListener(this);
        btnRequestPermission.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        switch (id){
            case R.id.btnCheckPermission:
                if (checkPermission(wantPermission)) {
                    Toast.makeText(context, "Permission already granted.", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(context, "Please request permission.", Toast.LENGTH_LONG).show();
                }
                break;
            case R.id.btnRequestPermission:
                if (!checkPermission(wantPermission)) {
                    requestPermission(wantPermission);
                } else {
                    Toast.makeText(context,"Permission already granted.", Toast.LENGTH_LONG).show();
                }
                break;
        }
    }

    private boolean checkPermission(String permission){
        if (Build.VERSION.SDK_INT >= 23) {
            int result = ContextCompat.checkSelfPermission(context, permission);
            if (result == PackageManager.PERMISSION_GRANTED){
                return true;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }

    private void requestPermission(String permission){
        if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)){
            Toast.makeText(context, "Write external storage permission allows us to write data. 
                Please allow in App Settings for additional functionality.",Toast.LENGTH_LONG).show();
        }
        ActivityCompat.requestPermissions(activity, new String[]{permission},PERMISSION_REQUEST_CODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(context, "Permission Granted. Now you can write data.", 
                        Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(context,"Permission Denied. You cannot write data.", 
                        Toast.LENGTH_LONG).show();
                }
                break;
        }
    }
}

If permission has been granted then the value of grantResults[0] would be PERMISSION_GRANTED. And if permission has been revoked then the value of grantResults[0] would be PERMISSION_DENIED.

Result

android_single_permission.png

Requesting multiple Permissions

Think of a scenario where you might need to ask for multiple permissions. For this, instead of requesting multiple permissions in multiple dialogs, we can prompt all the permissions in a single dialog where permissions will be scrolled through one after another. Now we’ll test this case by creating a new activity.

Add the below permissions to AndroidManifest.xml file.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"  />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Following is full code of updated MainActivity.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    final String TAG = "DPG";
    private Context context;
    private Activity activity;
    private final static int ALL_PERMISSIONS_RESULT = 101;
    ArrayList<String> permissions = new ArrayList<>();
    ArrayList<String> permissionsToRequest;
    ArrayList<String> permissionsRejected = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_permissions);

        context = this;
        activity = MainActivity.this;

        Button btnCheckPermission = (Button)findViewById(R.id.btnCheckPermission);
        Button btnRequestPermission = (Button)findViewById(R.id.btnRequestPermission);
        btnCheckPermission.setOnClickListener(this);
        btnRequestPermission.setOnClickListener(this);

        permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
        permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        permissionsToRequest = findUnAskedPermissions(permissions);
    }

    private ArrayList findUnAskedPermissions(ArrayList<String> wanted) {
        ArrayList result = new ArrayList();

        for (String perm : wanted) {
            if (!hasPermission(perm)) {
                result.add(perm);
            }
        }

        return result;
    }

    private boolean hasPermission(String permission) {
        if (canAskPermission()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
            }
        }
        return true;
    }

    private boolean canAskPermission() {
        return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
    }


    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case ALL_PERMISSIONS_RESULT:
                Log.d(TAG, "onRequestPermissionsResult");
                for (String perms : permissionsToRequest) {
                    if (!hasPermission(perms)) {
                        permissionsRejected.add(perms);
                    }
                }

                if (permissionsRejected.size() > 0) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) {
                            String msg = "These permissions are mandatory for the application. Please allow access."; 
                            showMessageOKCancel(msg,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                                requestPermissions(permissionsRejected.toArray(
                                                        new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT);
                                            }
                                        }
                                    });
                            return;
                        }
                    }
                } else {
                    Toast.makeText(context, "Permissions garanted.", Toast.LENGTH_LONG).show();
                }
                break;
        }
    }

    private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(activity)
                .setMessage(message)
                .setPositiveButton("OK", okListener)
                .setNegativeButton("Cancel", null)
                .create()
                .show();
    }

    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public void onClick(View v) {
        permissionsToRequest = findUnAskedPermissions(permissions);

        switch (v.getId()){
            case R.id.btnCheckPermission:
                if (permissionsToRequest.size() == 0) {
                    Toast.makeText(context, "Permissions already granted.", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(context, "Please request permissions.", Toast.LENGTH_LONG).show();
                }
                break;
            case R.id.btnRequestPermission:
                if (permissionsToRequest.size() > 0) {
                    requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]),
                            ALL_PERMISSIONS_RESULT);
                } else {
                    Toast.makeText(context,"Permissions already granted.", Toast.LENGTH_LONG).show();
                }
                break;
        }
    }


}
android_multiple_permission.png

Use Support Library to make code forward-compatible

Although the code above works perfectly on Android 6.0 Marshmallow. Unfortunate that it will crash on Android pre-Marshmallow since those functions called are added in API Level 23.

The straight way is you can check Build Version with code below.

if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

But code will be even more complicated. So I suggest you to use some help from Support Library v4 which is already prepared for this thing. Use those functions:

  • ContextCompat.checkSelfPermission(). No matter application is run on M or not. This function will correctly return PERMISSION_GRANTED if the permission is granted. Otherwise PERMISSION_DENIED will be returned.
  • ActivityCompat.requestPermissions(). If this function is called on pre-M, OnRequestPermissionsResultCallback will be suddenly called with correct PERMISSION_GRANTED or PERMISSION_DENIED result.
  • ActivityCompat.shouldShowRequestPermissionRationale(). If this function is called on pre-M, it will always return false.

Managing Permissions using ADB

Permissions can also be managed on the command-line using adb with the following commands.

Show all Android permissions:

adb shell pm list permissions -d -g

Dumping app permission state:

adb shell dumpsys package com.PackageName.enterprise

Granting and revoking runtime permissions:

adb shell pm grant com.PackageName.enterprise some.permission.NAME
adb shell pm revoke com.PackageName.enterprise android.permission.READ_CONTACTS

Installing an app with all permissions granted:

adb install -g myAPP.apk

EasyPermissions to handle runtime permission

EasyPermissions is a wrapper library to simplify basic system permissions logic when targeting Android M or higher. Using EasyPermissions to check if the app already has the required permissions. This method can take any number of permissions as its final argument.

EasyPermissions is installed by adding the following dependency to your build.gradle file:

dependencies {
    implementation 'com.android.support:design:27.0.1'
    implementation 'pub.devrel:easypermissions:1.2.0'
}

To begin using EasyPermissions, have your Activity (or Fragment) override the onRequestPermissionsResultmethod:

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks {
    private static final int RC_PERM_LOCATION_STORAGE = 124;
    private static final int RC_PERM_LOCATION = 125;
    private static final int RC_SETTINGS = 126;

    private String wantedPerm = Manifest.permission.ACCESS_FINE_LOCATION;
    private String[] wantedPerms = {Manifest.permission.ACCESS_FINE_LOCATION, 
        Manifest.permission.WRITE_EXTERNAL_STORAGE};
    private String PERM_RATIONALE = "This app needs access to your location.";

    private Activity activity = MainActivity.this;
    private String TAG = FontsActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fonts);

        if (EasyPermissions.hasPermissions(activity, wantedPerm)) {
            Log.d(TAG, "Already have permission, do the thing");
        } else {
            EasyPermissions.requestPermissions(activity, PERM_RATIONALE, RC_PERM_LOCATION, wantedPerm);
        }

    }

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

        // Forward results to EasyPermissions
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    @Override
    public void onPermissionsGranted(int requestCode, List perms) {
        Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
    }

    @Override
    public void onPermissionsDenied(int requestCode, List perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
        if (EasyPermissions.somePermissionPermanentlyDenied(activity, perms)) {
            new AppSettingsDialog.Builder(activity)
                    .setTitle("Permissions Required")
                    .setPositiveButton("Settings")
                    .setNegativeButton("Cancel")
                    .setRequestCode(RC_SETTINGS)
                    .build()
                    .show();
        }
    }

    @AfterPermissionGranted(RC_PERM_LOCATION)
    private void afterLocationPermission() {
        if (EasyPermissions.hasPermissions(this, wantedPerm)) {
            Log.d(TAG, "Already have permission, do the thing");
        } else {
            Log.d(TAG, "Do not have permission, request them now");
            EasyPermissions.requestPermissions(activity, PERM_RATIONALE, RC_PERM_LOCATION, wantedPerm);
        }
    }   
}

There are a few things to note:

  • Using EasyPermissions#hasPermissions() to check if the app already has the required permissions. This method can take any number of permissions as its final argument.
  • Requesting permissions with EasyPermissions#requestPermissions. This method will request the system permissions and show the rationale string provided if necessary. The request code provided should be unique to this request, and the method can take any number of permissions as its final argument.
  • Use of the AfterPermissionGranted annotation. This is optional, but provided for convenience. If all of the permissions in a given request are granted, any methods annotated with the proper request code will be executed. This is to simplify the common flow of needing to run the requesting method after all of its permissions have been granted. This can also be achieved by adding logic on the onPermissionsGrantedcallback.

Handle Android Runtime Permissions in Kotlin way

Let's use PermissionsKt for runtime permission in Kotlin way.

PermissionsKt is published with JitPack.io. To add this library to your project, add these lines to your build.gradle.

repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    implementation 'com.github.sembozdemir:PermissionsKt:1.0.0'
}

Add the permission to AndroidManifest.xml.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Just ask for permission you want anywhere on your Activity.

askPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
    onGranted {
        writeFile()
    }
}

Call delegated extension function on onRequestPermissionsResult.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 
        grantResults: IntArray) {
    handlePermissionsResult(requestCode, permissions, grantResults)
}

You may also listen onDeniedonShowRationaleonNeverAskAgain callbacks if you need.

Following is a full code for MainActivity.

class MainActivity : AppCompatActivity() {
    private val wantedPerm = Manifest.permission.WRITE_EXTERNAL_STORAGE
    private val TAG = MainActivity::class.java.name;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        askPermissions(wantedPerm) {
            onGranted {
                writeFile()
            }

            onDenied {
                Log.d(TAG, "WRITE_EXTERNAL_STORAGE permission is denied.")
            }

            onShowRationale { request ->
                Snackbar.make(rootView, "You should grant WRITE_EXTERNAL_STORAGE permission", 
                    Snackbar.LENGTH_INDEFINITE)
                        .setAction("Retry") { request.retry() }
                        .show()
            }

            onNeverAskAgain {
                Log.d(TAG, "Never ask again for WRITE_EXTERNAL_STORAGE permission")
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 
                           grantResults: IntArray) {
        handlePermissionsResult(requestCode, permissions, grantResults)
    }

    private fun writeFile() {
        longToast("Write file")
    }
}