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.
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:
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_STORAGE
permission.
Permission Group | Permissions |
---|---|
android.permission-group.CALENDAR |
|
android.permission-group.CAMERA |
|
android.permission-group.CONTACTS |
|
android.permission-group.LOCATION |
|
android.permission-group.MICROPHONE |
|
android.permission-group.PHONE |
|
android.permission-group.SENSORS |
|
android.permission-group.SMS |
|
android.permission-group.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 Snackbar
whether 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
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; } } }
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 onRequestPermissionsResult
method:
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, Listperms) { 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:
EasyPermissions#hasPermissions()
to check if the app already has the required permissions. This method can take any number of permissions as its final argument.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.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 onPermissionsGranted
callback.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 onDenied
, onShowRationale
, onNeverAskAgain
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") } }